mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-08 04:44:55 +10:00
Compare commits
193 Commits
dark-theme
...
v6.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
| cd9ac32a2f | |||
| 9922615de5 | |||
| 44b57f4880 | |||
| 486e83b6b5 | |||
| eb48ab9664 | |||
| a743de4024 | |||
| f62b3ddf04 | |||
| f77c821f0a | |||
| 208c157016 | |||
| c6d4b9df9c | |||
| 2fe6b50ab4 | |||
| fe267a2377 | |||
| 91fd8584f9 | |||
| 51698ef8af | |||
| b9344dfb0a | |||
| 69bb77d307 | |||
| 556dedc9a3 | |||
| 823a59d455 | |||
| 3ed51658a2 | |||
| d41aa02674 | |||
| 14bdeec281 | |||
| 158f0d2647 | |||
| 6d5cc3effc | |||
| c3d2949dab | |||
| b0f1a0bb2c | |||
| 27b53e60e3 | |||
| 89ce96b6c8 | |||
| 28d8441887 | |||
| 8080b8d70b | |||
| 001a26839f | |||
| 7b168e2e27 | |||
| 047196499a | |||
| 6f51c3ca42 | |||
| 26d6ca48fd | |||
| e209ecdcb8 | |||
| ac6c38b115 | |||
| cdc635b7da | |||
| 0598cc441c | |||
| 18b6bfb1db | |||
| 8d506b21c4 | |||
| b766c55263 | |||
| 09b53a0685 | |||
| 98d9319938 | |||
| 63ad65152a | |||
| 040e959d8d | |||
| 657521e7f5 | |||
| af4064912b | |||
| c34e5a2d38 | |||
| c37a2b8310 | |||
| 02237c9255 | |||
| ee188475d4 | |||
| 2e1787f5d0 | |||
| b1629680ff | |||
| 769bf1a381 | |||
| 85a17279b5 | |||
| f5b9faecd4 | |||
| 769770bd8a | |||
| fc103b6c2e | |||
| 64ffc3a826 | |||
| dbbd8a7f5a | |||
| e89eb1cbdf | |||
| 0476dbb876 | |||
| 1571d78048 | |||
| d8a2538b35 | |||
| 717892c38e | |||
| 896b6e5f69 | |||
| 475b829562 | |||
| aeabec7392 | |||
| 2261433bf0 | |||
| e85d618498 | |||
| 3425fff3a0 | |||
| cc09c59fe5 | |||
| 0219aa5c2a | |||
| 9b3274e30c | |||
| 9c29d2c4d6 | |||
| 1d7dd87d85 | |||
| 9179086bd5 | |||
| 3fbc7f5bc9 | |||
| 0526ce38f4 | |||
| 880fa1e9e1 | |||
| fb0821bce2 | |||
| 6a590af026 | |||
| efa803d7d0 | |||
| f481542d5a | |||
| 140c052d39 | |||
| 75edc427f7 | |||
| 717c16e88a | |||
| ffa8e34497 | |||
| 9de5fe6ebc | |||
| 0cdbcd60fe | |||
| 491d48f0ec | |||
| 9f6a0c537a | |||
| e674a1f23f | |||
| 9481b7e6a3 | |||
| 833ef947d5 | |||
| c092b5f56e | |||
| 5546a66c94 | |||
| b034c8c761 | |||
| 8318b34967 | |||
| 194c8e0606 | |||
| 124d1ab577 | |||
| 5ce17b4ca2 | |||
| 30c6b4ec76 | |||
| 2001a1af7b | |||
| 27f5c53afc | |||
| 10cbbea3d3 | |||
| b26a489f3a | |||
| dab4975347 | |||
| 5d8555e316 | |||
| 614652ca97 | |||
| af3887b9ac | |||
| 150e03c023 | |||
| cd0cb776b0 | |||
| ebd66eea4f | |||
| 7d24621c18 | |||
| a7442e14e4 | |||
| 03af702933 | |||
| 303e59303b | |||
| 508d1db513 | |||
| c4efe7d20f | |||
| fccda215fc | |||
| d89061b355 | |||
| f0c482d2d5 | |||
| f5fedc1a24 | |||
| b3a1187c1c | |||
| 7e79143079 | |||
| d976959e95 | |||
| 8b69903893 | |||
| 391d161c59 | |||
| d970fa10a7 | |||
| b6816e6a62 | |||
| 3272efc8bf | |||
| 0aed29f072 | |||
| 7e2319746e | |||
| 1cf776ace5 | |||
| b9da7510a8 | |||
| 6fde016ed4 | |||
| a32ad6b733 | |||
| be16910767 | |||
| 457f47782f | |||
| b69729c14f | |||
| a88d8f5e03 | |||
| f4223f5773 | |||
| 513cc67362 | |||
| d8f2a3b6e0 | |||
| 7900128d7c | |||
| 1cef0c0977 | |||
| 2383f9ae77 | |||
| 14ec18dfef | |||
| 2f492c8fa8 | |||
| 9db43cd2f4 | |||
| 344c238f73 | |||
| 3376aa995a | |||
| ec75f1b6eb | |||
| 5f2b20ea41 | |||
| 9d130c0ff3 | |||
| 119c41dc86 | |||
| c5613a6fea | |||
| 479bd50540 | |||
| 502a4b8d39 | |||
| 1f85c07453 | |||
| 53ec1e780e | |||
| 2de3def32e | |||
| 61b3c63bbf | |||
| 348eb3ac00 | |||
| 9972e0ee46 | |||
| de971b707b | |||
| 0dcc06f712 | |||
| 109b0788a8 | |||
| d190ebe208 | |||
| fa93af2707 | |||
| 6b59966816 | |||
| b065c9d6fb | |||
| 49d805a60c | |||
| a9a98bf3a4 | |||
| 840d416282 | |||
| dc2cc4c7c8 | |||
| 3d51d2a38d | |||
| 4dfb1ca630 | |||
| e9c8686960 | |||
| 9be97f5213 | |||
| 721d582dcd | |||
| 443f76f3f9 | |||
| 3ad9c49f53 | |||
| 61077ed5a4 | |||
| 1fe3706d0f | |||
| 16df8fd173 | |||
| f7806d4d3c | |||
| 04e44e174e | |||
| f74fe19b77 | |||
| e7487069a5 | |||
| 9c48b80515 | |||
| 814f4296a9 |
@@ -14,10 +14,10 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
|
||||
|
||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
"java.configuration.updateBuildConfiguration": "disabled",
|
||||
"java.compile.nullAnalysis.mode": "disabled"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# DIY Layout Creator — repository map
|
||||
|
||||
The guidelines that govern all code changes live in **[`diylc/CLAUDE.md`](diylc/CLAUDE.md)**. Read
|
||||
that file before editing anything under `diylc/`.
|
||||
|
||||
## What is where
|
||||
|
||||
| Path | Contents |
|
||||
| --- | --- |
|
||||
| `diylc/` | The application. Maven reactor with `diylc-core`, `diylc-library`, `diylc-swing`. This is where nearly all work happens. |
|
||||
| `diylc-regression-data/` | Sample `.diy` projects plus reference PNG and netlist outputs used by the regression suite, and the historical reports. |
|
||||
| `diylc-server-api/` | PHP backend for cloud and collaboration features. Runs in an isolated environment on diy-fever.com and cannot be tested locally — do not modify unless explicitly asked. |
|
||||
| `snap/` | Snap packaging metadata. |
|
||||
|
||||
## House rules that apply repository-wide
|
||||
|
||||
- Match the conventions of the surrounding code; consistency beats novelty.
|
||||
- Comment only what the code cannot say for itself. No comments that restate the line below them.
|
||||
- Every new source file begins with the GPL header from `diylc/HEADER.txt`.
|
||||
- Do not commit or push. Leave changes in the working tree for review.
|
||||
|
||||
The project is GPLv3; see `COPYING`.
|
||||
@@ -1,63 +1,93 @@
|
||||
<h3><a name="News"></a>Introduction<a href="#Introduction" class="section_anchor"></a></h3>
|
||||
<p align="justify">
|
||||
DIY Layout Creator (DIYLC) is a specialized software tool designed for hobbyists and electronics enthusiasts to create circuit layouts on stripboard, perfboard, PCB, and point-to-point wiring. It provides an intuitive, drag-and-drop interface for placing components, arranging connections, and visualizing circuit designs before assembly. With a built-in component library and customization options, it allows users to design layouts with precision and clarity. The software is particularly useful for designing, analyzing, and debugging guitar wiring diagrams, helping musicians and builders plan and troubleshoot custom wiring configurations for pickups, switches, and controls. Its versatility makes it an ideal solution for DIY electronics projects, prototyping, and documentation.</p>
|
||||
<br>
|
||||
<a href="https://github.com/bancika/diy-layout-creator/releases/latest">Click here</a> to get the latest version for all platforms.</a><br>
|
||||
<a href="https://github.com/bancika/diy-layout-creator/tree/wiki">Click here</a> to read the user documentation as well as developer documentation.<br>
|
||||
<br>
|
||||
<b>Mac users</b>: in most instances, the Gatekeeper will block DIYLC and mark it as damaged because it's a 3rd party app from an independent vendor (me). <a href="[https://support.apple.com/en-us/HT202491](https://support.apple.com/kb/ph18657?locale=en_US)">This document</a> covers how to allow DIYLC on your Mac.
|
||||
### 🛠️ Introduction
|
||||
|
||||
<h3><a name="Samples"></a>Samples<a href="#Samples" class="section_anchor"></a></h3>
|
||||
DIY Layout Creator (DIYLC) is a specialized software tool designed for hobbyists and electronics enthusiasts to create circuit layouts on stripboard, perfboard, PCB, and point-to-point wiring.
|
||||
|
||||
<h4>Point-to-point tube amplifier</h4>
|
||||
<p align="center"><a href="http://diy-fever.com/wordpress/wp-content/gallery/diylc/slo.png" rel="nofollow"><img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/cache/slo.png-nggid041072-ngg0dyn-450x350-00f0w010c010r110f110r010t010.png"></a></p>
|
||||
<h4>Guitar wiring</h4>
|
||||
<p align="center"><a href="http://diy-fever.com/wordpress/wp-content/gallery/diylc/travelcaster.png" rel="nofollow"><img src="https://diy-fever.com/nextgen-image/1073/450x350/34b70d759e46422208d61e376b4633dc"></a></p>
|
||||
<h4>PCB tube amplifier</h4>
|
||||
<p align="center"><a href="http://diy-fever.com/wordpress/wp-content/gallery/diylc/iic.png" rel="nofollow"><img src="https://diy-fever.com/nextgen-image/1070/450x350/ccb592ec5828b6f5bc75e840438918f1"></a></p>
|
||||
<h4>Perfboard guitar effect</h4>
|
||||
<p align="center"><a href="http://diy-fever.com/wordpress/wp-content/gallery/diylc/split.png" rel="nofollow"><img src="https://diy-fever.com/nextgen-image/1069/450x350/358e43165de40e9c4043726c9efde4a7"></a></p>
|
||||
It provides an intuitive, drag-and-drop interface for placing components, arranging connections, and visualizing circuit designs before assembly. With a built-in component library and customization options, it allows users to design layouts with precision and clarity.
|
||||
|
||||
<h3><a name="Key Features"></a>Features<a href="#Features" class="section_anchor"></a></h3>
|
||||
<ul>
|
||||
<li>platform independence, will run on any machine having Java JRE/JDK 8 or newer </li>
|
||||
<li>easy to use interface, most of the operations can be done using mouse </li>
|
||||
<li>draw PCB, perfboard, stripboard or point-to-point circuit layouts layouts, schematics and guitar wiring diagrams </li>
|
||||
<li>high flexibility, has the API to allow plug-ins and new components to be added</li>
|
||||
<li>improved performance and reduced memory consumption compared to the previous versions </li>
|
||||
<li>save default values for each property of a component individually </li>
|
||||
<li>group components together and treat them as one, e.g. move, edit or delete </li>
|
||||
<li>export the output to image, PDF or printer </li>
|
||||
<li>export PCB trace mask suitable for toner transfer </li>
|
||||
<li>export PCB designs to Gerber for fabrication </li>
|
||||
<li>create a Bill of Materials as a part of the project or export it to few different file formats </li>
|
||||
<li>analyze guitar wirings and show detailed pickup configuration in each switch configuration</li>
|
||||
<li>zoom in/out feature </li>
|
||||
<li>configurable grid spacing on the project level </li>
|
||||
<li>auto update checks for new versions </li>
|
||||
<li>import files created with older versions of the application </li>
|
||||
<li>cloud feature - share your projects, search through the cloud and download other users' projects </li>
|
||||
<li>highlight connected areas to simplify validation and circuit debugging</li>
|
||||
<li>analyze guitar wiring diagrams and visualize pickup wirings for each switch configuration</li>
|
||||
<li>export circuits to SPICE-compatible netlists</li>
|
||||
</ul>
|
||||
<p>To report bugs or request a new feature go to <a href="https://github.com/bancika/diy-layout-creator/issues" rel="nofollow">Issues</a> page and create a new issue describing your problem or request. </p>
|
||||
<p>To join the discussion or request technical assistance, use the dedicated <a href="http://groups.google.com/group/diy-layout-creator" rel="nofollow">Discussion Group</a>. </p>
|
||||
<h3><a name="Want_to_contribute?"></a>Want to contribute?<a href="#Want_to_contribute?" class="section_anchor"></a></h3>
|
||||
<p align="justify"><strong>DIYLC is provided free of charge and can be used and redistributed freely. Please note that it takes a lot of time and effort to build, test and maintain it and to interact with the growing community of users. If you find DIYLC useful and want to support further development, these are some of the ways you can contribute</strong></p>
|
||||
<ul>
|
||||
<li><a href="https://www.patreon.com/c/user?u=5213116">Become a Patreon.</a></li>
|
||||
<li><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=U6GLS8HLTSU88">Make a one-time donation with PayPal.</a></li>
|
||||
<li>Help with adding the information to the <a href="https://github.com/bancika/diy-layout-creator/blob/wiki/Manual.md">user manual</a> or other project documentation.</li>
|
||||
<li>Extend the component base, read <a href="https://github.com/bancika/diy-layout-creator/blob/wiki/ComponentAPI.md" rel="nofollow">this wiki</a> to learn how to create new components. </li>
|
||||
<li>Add a new functionality though plug-ins, read <a href="https://github.com/bancika/diy-layout-creator/blob/wiki/PluginAPI.md" rel="nofollow">this wiki</a> to learn how to create new plug-ins. </li>
|
||||
</ul>
|
||||
<!--<p align="center"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=U6GLS8HLTSU88" rel="nofollow"><img src="https://www.paypal.com/en_US/i/btn/btn_donateCC_LG.gif"></a> </p>-->
|
||||
<h3><a name="License_information"></a>License information<a href="#License_information" class="section_anchor"></a></h3>
|
||||
<p>Source code is released under <a href="https://www.gnu.org/licenses/gpl-3.0.txt">GNU General Public License version 3</a>. Contact the project owner to obtain a license in case you plan to use the source code, or any part of it, in commercial purposes. </p>
|
||||
<p></p>
|
||||
<h3><a name="Credits"></a>Credits<a href="#Credits" class="section_anchor"></a></h3>
|
||||
<ul>
|
||||
<li>Big thanks goes to folks from the following online communities for contributing ideas and helped test the app: <a href="http://www.diystompboxes.com/smfforum/" rel="nofollow">DIY Stompboxes</a>, <a href="http://ax84.com/bbs" rel="nofollow">AX84</a> and <a href="http://freestompboxes.org" rel="nofollow">Free Stompboxes</a>. </li>
|
||||
<li>I'm happy to report that DIYLC 4 is the fastest version ever. <a href="http://www.yourkit.com/java/profiler/index.jsp" rel="nofollow">YourKit Java Profiler</a> helped tremendously with memory and performance profiling and pointed me to places in the code that took longer to run or consumed more memory than I'd like them to. I strongly recommend this tool to anyone developing Java or .NET applications because it helps finding bottlenecks and allocation hot-spots very fast.<br><br>YourKit is kindly supporting open source projects with its full-featured Java Profiler.<br>YourKit, LLC is the creator of innovative and intelligent tools for profiling Java and .NET applications.<br>Take a look at YourKit's leading software products: <a href="http://www.yourkit.com/java/profiler/index.jsp" rel="nofollow">YourKit Java Profiler</a> and <a href="http://www.yourkit.com/.net/profiler/index.jsp" rel="nofollow">YourKit .NET Profiler</a> </li>
|
||||
</ul>
|
||||
🎸 **Perfect for guitar pedal builders, guitar, synth, audio and HAM enthusiasts!** DIYLC is particularly useful for designing, analyzing, and debugging guitar wiring diagrams, helping musicians and builders plan and troubleshoot custom wiring configurations for pickups, switches, and controls. Its versatility makes it an ideal solution for DIY electronics projects, prototyping, and documentation.
|
||||
|
||||
### 🚀 Quick Links
|
||||
* **[⬇️ Download the latest release](https://github.com/bancika/diy-layout-creator/releases/latest)** for all platforms.
|
||||
* **[📄 Read the Wiki](https://github.com/bancika/diy-layout-creator/tree/wiki)** for free user and developer documentation.
|
||||
|
||||
> 🍎 **Mac users:** Gatekeeper may block DIYLC since it's from an independent developer. [Read this guide](https://support.apple.com/kb/ph18657?locale=en_US) on how to easily allow DIYLC on your Mac.
|
||||
|
||||
---
|
||||
|
||||
### 📖 The Official DIYLC Guide
|
||||
|
||||
<a href="https://www.diy-fever.com/diylc-book">
|
||||
<img src="https://www.diy-fever.com/diylc/book/book_v1_thumb.png" alt="DIYLC Official Guide" width="220" align="left" style="margin-right: 20px; margin-bottom: 20px;">
|
||||
</a>
|
||||
|
||||
**Master DIY Layout Creator and streamline your design workflow!**
|
||||
|
||||
The **Official DIYLC Guide (v1.0)** is a comprehensive 174-page eBook that goes way beyond the basics. It features step-by-step design workflows for everything from multi-layer PCBs to hot-rodded point-to-point tube amps. Learn to master advanced tools like the Virtual Multimeter, Netlist generation, the Guitar Wiring Analyzer, and the new AI Assistant.
|
||||
|
||||
👉 **[Click here to grab your copy!](https://www.diy-fever.com/diylc-book)**
|
||||
|
||||
*Active Patreon supporters get an additional 50% off or a completely free copy. Check the [Patreon feed](https://www.patreon.com/c/user?u=5213116) for your VIP link!*
|
||||
|
||||
<br clear="all">
|
||||
|
||||
---
|
||||
|
||||
### 🖼️ Gallery & Samples
|
||||
|
||||
<p align="center">
|
||||
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/slo.png" width="48%" alt="Point-to-point tube amplifier">
|
||||
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/travelcaster.png" width="48%" alt="Guitar wiring">
|
||||
<br>
|
||||
<sup><i>Left: Point-to-point tube amplifier | Right: Guitar wiring</i></sup>
|
||||
<br><br>
|
||||
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/iic.png" width="48%" alt="PCB tube amplifier">
|
||||
<img src="https://diy-fever.com/wordpress/wp-content/gallery/diylc/split.png" width="48%" alt="Perfboard guitar effect">
|
||||
<br>
|
||||
<sup><i>Left: PCB tube amplifier | Right: Perfboard guitar effect</i></sup>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
### ✨ Key Features
|
||||
* 🆓 **Free for Life**: A true old-school offline app—the app you download is yours forever and the files you created are yours.
|
||||
* 💻 **Platform Independence:** Runs locally on any machine capable of running Java JRE/JDK 21 or newer.
|
||||
* 🖱️ **Intuitive UI:** Easy-to-use interface; most operations can be done seamlessly with a mouse.
|
||||
* 🎛️ **Versatile Designs:** Draw PCB, perfboard, stripboard, or point-to-point layouts, schematics, guitar wiring diagrams or custom faceplates.
|
||||
* 🔌 **Highly Extensible:** API allows custom plug-ins and new components to be added easily.
|
||||
* 🚀 **Optimized:** Improved performance and reduced memory consumption.
|
||||
* 💾 **Smart Defaults:** Save default values individually for each component property.
|
||||
* 🖨️ **Export Options:** Export outputs to image, PDF, or printer.
|
||||
* 📄 **Toner Transfer & Gerber:** Export PCB trace masks for toner transfer or Gerber files for fabrication.
|
||||
* 📋 **Bill of Materials:** Create a BOM within the project or export to multiple file formats.
|
||||
* 🔍 **Guitar Wiring Analysis:** Analyze guitar wirings and visualize detailed pickup configurations for each switch state.
|
||||
* ☁️ **Cloud Integration:** Share your projects, search the cloud, and download other users' designs.
|
||||
* 🤖 **AI Powered:** Features a built-in AI assistant to help answer general electronics questions, guide you through DIYLC's usability, and provide insights or troubleshooting for the specific circuit you are designing.
|
||||
* ⚡ **Circuit Analysis:** Various tools aimed to help you debug and analyze your circuits.
|
||||
* 📈 **SPICE Netlists:** Export circuits directly to SPICE-compatible netlists.
|
||||
|
||||
---
|
||||
|
||||
### 🐛 Support & Community
|
||||
* **Found a bug or have a feature request?** Head over to the [Issues page](https://github.com/bancika/diy-layout-creator/issues) to open a ticket.
|
||||
* **Need technical assistance or want to chat?** Join the discussion in our [Google Group](http://groups.google.com/group/diy-layout-creator).
|
||||
|
||||
---
|
||||
|
||||
### ❤️ Want to contribute?
|
||||
DIYLC is provided free of charge and can be used and redistributed freely. Building, testing, and maintaining this tool—while supporting our growing community—takes a significant amount of time and effort.
|
||||
|
||||
If you find DIYLC useful, here are the best ways you can support its future development:
|
||||
* 🧡 **[Become a Patreon](https://www.patreon.com/c/user?u=5213116):** Join the inner circle for exclusive perks.
|
||||
* 📖 **[Purchase the Official DIYLC Guide](https://www.diy-fever.com/diylc-book):** Level up your layout skills while directly supporting the app!
|
||||
* ☕ **[Make a PayPal donation](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=U6GLS8HLTSU88):** A one-time donation helps keep the lights on.
|
||||
* 📝 **Documentation:** Help us improve the [user manual](https://github.com/bancika/diy-layout-creator/blob/wiki/Manual.md) or other project docs.
|
||||
* 🧩 **Extend the App:** Read the [Component API Wiki](https://github.com/bancika/diy-layout-creator/blob/wiki/ComponentAPI.md) to add new components, or the [Plugin API Wiki](https://github.com/bancika/diy-layout-creator/blob/wiki/PluginAPI.md) to create new plug-ins.
|
||||
|
||||
---
|
||||
|
||||
### 📜 License Information
|
||||
The source code is released under the [GNU General Public License version 3](https://www.gnu.org/licenses/gpl-3.0.txt). Please contact the project owner to obtain a commercial license if you plan to use the source code, or any part of it, for commercial purposes.
|
||||
|
||||
### 🙌 Credits
|
||||
* Huge thanks to the online communities that contributed ideas and helped test the app: [DIY Stompboxes](http://www.diystompboxes.com/smfforum/), [AX84](http://ax84.com/bbs), and [Free Stompboxes](http://freestompboxes.org).
|
||||
* DIYLC is highly optimized thanks to [YourKit Java Profiler](http://www.yourkit.com/java/profiler/index.jsp). YourKit is kindly supporting open-source projects with its full-featured Java Profiler, which helped tremendously with memory and performance profiling. *YourKit, LLC is the creator of innovative and intelligent tools for profiling Java and .NET applications.*
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: add-component
|
||||
description: Add a new component type to the DIYLC library, or substantially modify an existing one. Use when asked to create a new part (resistor variant, board, jack, switch, pickup, symbol, shape), add editable properties to a component, change how a component draws or reports connectivity, or make a component rotatable/mirrorable.
|
||||
---
|
||||
|
||||
# Adding a component to DIYLC
|
||||
|
||||
Components live in `diylc-library/src/main/java/org/diylc/components/`. They are discovered at
|
||||
runtime by Reflections scanning `org.diylc` — **there is no registry to update**. A class becomes a
|
||||
component by carrying `@ComponentDescriptor` and implementing `IDIYComponent` (in practice, by
|
||||
extending one of the abstract bases).
|
||||
|
||||
## Before writing anything
|
||||
|
||||
1. **Find the closest existing component and read it end to end.** A new axial part follows
|
||||
`passive/Resistor.java`; a new board follows `boards/VeroBoard.java`; a new schematic symbol
|
||||
follows `AbstractSchematicLeadedSymbol` subclasses; a new jack follows `electromechanical/`.
|
||||
The new class should read like a sibling of that file.
|
||||
2. **Ask whether it is really a new class.** If the request is a revision or size variant of
|
||||
something that already exists, prefer adding a `Version` (or similar enum) property to the
|
||||
existing class over creating a parallel class. That keeps old project files loading and keeps the
|
||||
toolbox from sprawling.
|
||||
3. **Pick the right package** — `passive`, `semiconductors`, `electromechanical`, `boards`, `guitar`,
|
||||
`tube`, `chassis`, `shapes`, `smd`, `connectivity`, `misc`, `autocreate`. The package should match
|
||||
the `category` in the descriptor.
|
||||
|
||||
## Choosing a base class
|
||||
|
||||
| Base | Use for |
|
||||
| --- | --- |
|
||||
| `AbstractComponent<T>` | Anything that fits nothing else |
|
||||
| `AbstractLabeledComponent<T>` | Adds name/value label rendering |
|
||||
| `AbstractTransparentComponent<T>` | Anything with an alpha/transparency property |
|
||||
| `AbstractLeadedComponent<T>` | Two-lead axial parts drawn between two points; implement `getDefaultLength()`, `getDefaultWidth()`, `getBodyShape()` |
|
||||
| `AbstractRadialComponent<T>` | Radial (two leads out the same side) parts |
|
||||
| `AbstractCurvedComponent<T>` | Curves and traces defined by control points |
|
||||
| `AbstractAngledComponent<T>` | Parts placed by point and angle |
|
||||
| `AbstractBoard` / `AbstractProtoBoard` / `AbstractVeroBoard` | Boards |
|
||||
| `AbstractMultiPartComponent<T>` | Components made of several independently drawn sections |
|
||||
| `Abstract3LegSymbol`, `AbstractDiodeSymbol`, `AbstractTubeSymbol`, `AbstractGuitarPickup`, `AbstractShape` | The obvious specialisations |
|
||||
|
||||
Extend the most specific base that fits. Do not reimplement behaviour a base already provides.
|
||||
|
||||
## The class
|
||||
|
||||
```java
|
||||
/* ... GPL header copied verbatim from HEADER.txt ... */
|
||||
package org.diylc.components.passive;
|
||||
|
||||
import ... // java.*, then third-party, then org.diylc.*
|
||||
|
||||
@ComponentDescriptor(name = "My Part", author = "Branislav Stojkovic", category = "Passive",
|
||||
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "X",
|
||||
description = "Short sentence shown in the toolbox tooltip", zOrder = IDIYComponent.COMPONENT,
|
||||
transformer = SimpleComponentTransformer.class, enableCache = true)
|
||||
public class MyPart extends AbstractLeadedComponent<Resistance> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static Size DEFAULT_WIDTH = new Size(1d / 2, SizeUnit.in);
|
||||
public static Color BODY_COLOR = Color.decode("#82CFFD");
|
||||
|
||||
private Resistance value = null;
|
||||
|
||||
public MyPart() {
|
||||
super();
|
||||
this.bodyColor = BODY_COLOR;
|
||||
}
|
||||
|
||||
@EditableProperty(validatorClass = PositiveMeasureValidator.class)
|
||||
public Resistance getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(Resistance value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) { ... }
|
||||
|
||||
public enum Version { // nested types last, just before the closing brace
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Requirements the framework enforces:
|
||||
|
||||
- **No-argument public constructor**, and the class must be serializable with a
|
||||
`private static final long serialVersionUID = 1L;`.
|
||||
- `@ComponentDescriptor` needs `name`, `description`, `category`, `author`, `instanceNamePrefix` and
|
||||
`zOrder` at minimum. Set `enableCache = true` for anything whose drawing is expensive and does not
|
||||
depend on neighbouring components.
|
||||
- Appearance constants are `public static` and deliberately **not final**, so they can be overridden
|
||||
at runtime. Follow that.
|
||||
- Nested types — a `Version` enum, a package or size enum, a small helper class — go at the **bottom
|
||||
of the file**, after every method, so the outer class reads top to bottom without a block of enum
|
||||
declarations in the middle of it.
|
||||
|
||||
## Editable properties
|
||||
|
||||
- Annotate the **getter** with `@EditableProperty`; a matching `setXYZ` must exist. The property name
|
||||
comes from the getter unless you pass `name`.
|
||||
- Use `sortOrder` when the order in the editor matters, `defaultable = false` for properties that
|
||||
must not be remembered as a user default, and `validatorClass` (e.g.
|
||||
`PositiveMeasureValidator`, `PositiveNonZeroMeasureValidator`) where a raw value would be invalid.
|
||||
- Use the measure types from `org.diylc.core.measures` (`Size`, `Resistance`, `Capacitance`,
|
||||
`Voltage`, `Power`, `Percentage`) rather than raw numbers — they carry units and format themselves.
|
||||
- For a runtime-computed list of allowed values, add `@DynamicList(availableValueFunction = "getXyz")`
|
||||
alongside `@EditableProperty`. (The `additionalOptions` example in `EditableProperty`'s own Javadoc
|
||||
is stale — that attribute no longer exists.)
|
||||
- Override `getValueForDisplay()` when the canvas label should combine several properties.
|
||||
|
||||
## Drawing
|
||||
|
||||
`draw(Graphics2D, ComponentState, boolean outlineMode, Project, IDrawingObserver)` runs on every
|
||||
repaint, for every instance. Treat it as a hot path.
|
||||
|
||||
- Fetch strokes from `ObjectCache.getInstance().fetchBasicStroke(w)`, `fetchZoomableStroke(w)` or
|
||||
`fetchStroke(w, dash, phase, cap)`. Do not allocate `new BasicStroke(...)` in `draw()`.
|
||||
- Honour `outlineMode` and every `ComponentState` (`NORMAL`, `SELECTED`, `DRAGGING`) the way the
|
||||
neighbouring components do.
|
||||
- `drawIcon(g2d, width, height)` is a separate, hand-drawn miniature for the toolbox — it is not a
|
||||
scaled call into `draw()`.
|
||||
|
||||
### Connectivity
|
||||
|
||||
If the component conducts, wrap the conductive drawing in
|
||||
`drawingObserver.startTrackingContinuityArea(true)` / `stopTrackingContinuityArea()`, exactly as the
|
||||
comparable component does. Getting this wrong silently corrupts continuity checks and the netlist
|
||||
without any visible rendering difference. `startTracking()` / `stopTracking()` control whether
|
||||
drawing contributes to the component's clickable area.
|
||||
|
||||
Also implement, where relevant:
|
||||
|
||||
- `getControlPointCount()`, `getControlPoint(int)`, `setControlPoint(Point2D, int)`,
|
||||
`isControlPointSticky(int)`, `getControlPointNodeName(int)` — the connection points.
|
||||
- `getInternalLinkName(int, int)` and `getSectionNames(int)` — internal conduction between points,
|
||||
needed by the netlist and analyzers.
|
||||
- `IGerberComponentSimple` or `IGerberComponentCustom` if the part should appear in Gerber output.
|
||||
|
||||
## Rotation and mirroring
|
||||
|
||||
These live in an `IComponentTransformer` under
|
||||
`diylc-library/src/main/java/org/diylc/components/transform/`, referenced from the `transformer`
|
||||
attribute of `@ComponentDescriptor` — never inside the component itself. Reuse
|
||||
`SimpleComponentTransformer` or an existing transformer where the behaviour matches; write a new one
|
||||
only when the part rotates or mirrors differently from everything that exists.
|
||||
|
||||
## Datasheets and variants
|
||||
|
||||
If the part has a catalogue of real-world values and sizes, set `enableDatasheet = true` and
|
||||
`datasheetCreationStepCount`, and add the file under
|
||||
`diylc-library/src/main/resources/datasheets/`. Follow the format of an existing `.datasheet` file
|
||||
and add a spot-check to `DatasheetServiceTest`.
|
||||
|
||||
## Backward compatibility
|
||||
|
||||
Once a component ships, its serialized shape is a contract:
|
||||
|
||||
- Never rename, remove, or retype a serialized field, and never rename or move the class.
|
||||
- To replace a property, keep the old field, mark the old accessors `@Deprecated`, add the new field,
|
||||
and migrate lazily in the new getter — see `AbstractTransparentComponent.getAlpha()`,
|
||||
`Resistor.getPowerNew()`, `AbstractFilmCapacitor.getVoltageNew()`.
|
||||
- Fields added later arrive as `null` from old files; default them in the getter, not the
|
||||
constructor.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
mvn -q -pl diylc-library -am test
|
||||
```
|
||||
|
||||
Then state plainly that the component still needs a visual check in the running app — automated tests
|
||||
do not cover component appearance. Offer a regression-suite run (see the `regression-test` skill) for
|
||||
any change to an existing component's drawing or serialization.
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
name: add-plugin
|
||||
description: Add or modify a DIYLC Swing plugin, menu action, toolbar item, dialog or side panel. Use when asked to add a UI feature, a menu entry, a new tool window, a background task, or to react to domain events like selection or project changes.
|
||||
---
|
||||
|
||||
# Adding a plugin or UI feature to DIYLC
|
||||
|
||||
The Swing module is a shell that hosts plugins. Everything the user sees — the toolbox, the canvas,
|
||||
every menu, the status bar, autosave — is an `IPlugIn`. Adding UI means adding or extending a plugin,
|
||||
not reaching into frames.
|
||||
|
||||
## Where things go
|
||||
|
||||
| Path | Contents |
|
||||
| --- | --- |
|
||||
| `diylc-swing/src/main/java/org/diylc/swing/plugins/<name>/` | One package per plugin |
|
||||
| `diylc-swing/src/main/java/org/diylc/swing/actions/` | `AbstractAction` implementations, grouped into `file`, `edit`, `analyze` sub-packages |
|
||||
| `diylc-swing/src/main/java/org/diylc/swing/gui/` | `MainFrame`, dialogs, editors, shared components |
|
||||
| `diylc-swing/src/main/java/org/diylc/swing/gui/MainFrame.java` | Where plugins are installed |
|
||||
|
||||
## Before writing anything
|
||||
|
||||
Read the plugin closest to what is being asked. `plugins/autosave/AutoSavePlugin.java` is the
|
||||
smallest complete example (event subscription plus background work);
|
||||
`plugins/statusbar/`, `plugins/layers/` and `plugins/analyze/` show menu and panel injection.
|
||||
Match the one you read.
|
||||
|
||||
## The plugin
|
||||
|
||||
```java
|
||||
/* ... GPL header from HEADER.txt ... */
|
||||
package org.diylc.swing.plugins.myfeature;
|
||||
|
||||
public class MyFeaturePlugin implements IPlugIn {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MyFeaturePlugin.class);
|
||||
|
||||
private ISwingUI swingUI;
|
||||
private IPlugInPort plugInPort;
|
||||
|
||||
public MyFeaturePlugin(ISwingUI swingUI) {
|
||||
this.swingUI = swingUI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(IPlugInPort plugInPort) {
|
||||
this.plugInPort = plugInPort;
|
||||
swingUI.injectMenuAction(new MyAction(plugInPort), "Tools");
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnumSet<EventType> getSubscribedEventTypes() {
|
||||
return EnumSet.of(EventType.SELECTION_CHANGED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processMessage(EventType eventType, Object... params) {
|
||||
if (eventType == EventType.SELECTION_CHANGED) {
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Install it in `MainFrame` alongside the others:
|
||||
|
||||
```java
|
||||
presenter.installPlugin(() -> new MyFeaturePlugin(this));
|
||||
```
|
||||
|
||||
Order matters for menus and for docked panels — insert the line where the resulting menu or panel
|
||||
position is correct, not blindly at the end.
|
||||
|
||||
## Talking to the domain
|
||||
|
||||
Plugins reach the domain **only** through `IPlugInPort`. Never import a concrete component type into
|
||||
a plugin unless the feature is genuinely component-specific, and never call into `Presenter`
|
||||
directly. If `IPlugInPort` lacks what you need, adding a method there is the right move — but it also
|
||||
means touching `diylc-core`, so keep the addition minimal and in the style of its neighbours.
|
||||
|
||||
## Reacting to events
|
||||
|
||||
`getSubscribedEventTypes()` returns an `EnumSet<EventType>`. Subscribe to the narrowest set that
|
||||
works. `PROJECT_MODIFIED`, `REPAINT` and `MOUSE_MOVED` fire constantly — anything non-trivial in
|
||||
response to those will be felt as lag. The available types are in
|
||||
`diylc-core/src/main/java/org/diylc/common/EventType.java` (`PROJECT_LOADED`, `PROJECT_SAVED`,
|
||||
`SELECTION_CHANGED`, `ZOOM_CHANGED`, `SLOT_CHANGED`, `LAYER_STATE_CHANGED`, `CLOUD_LOGGED_IN`, …).
|
||||
|
||||
## Injecting UI
|
||||
|
||||
- `swingUI.injectMenuAction(action, "Menu Name")` — a menu item; passing a `null` action inserts a
|
||||
separator, and a missing menu is created automatically.
|
||||
- `swingUI.injectSubmenu(name, icon, parentMenuName)` and
|
||||
`injectDynamicSubmenu(name, icon, parentMenuName, handler)` for submenus.
|
||||
- `swingUI.injectGUIComponent(component, SwingConstants.LEFT, collapsible, visibilityConfigKey)` for
|
||||
a docked panel. `TOP`, `BOTTOM`, `LEFT`, `RIGHT` are valid; `CENTER` is reserved for the canvas.
|
||||
Pass a config key for collapsible panels so the open/closed state persists.
|
||||
- `swingUI.getOwnerFrame()` is the parent for dialogs.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are small classes extending `javax.swing.AbstractAction`, holding an `IPlugInPort`, setting
|
||||
their label with `putValue(AbstractAction.NAME, ...)`, and delegating to `plugInPort` in
|
||||
`actionPerformed`. See `actions/RenumberAction.java` for the minimal shape. They carry
|
||||
`private static final long serialVersionUID = 1L;` like any Swing class.
|
||||
|
||||
## Background work
|
||||
|
||||
Never block the EDT. Use `swingUI.executeBackgroundTask(task, blockUI)` with an `ITask<T>`, which
|
||||
handles the busy cursor and optional glass pane, or a dedicated executor for periodic work as
|
||||
`AutoSavePlugin` does.
|
||||
|
||||
## Settings and strings
|
||||
|
||||
- Persisted preferences: `ConfigurationManager.getInstance().readString(KEY, default)` and
|
||||
`writeValue(KEY, value)`, with the key as a `String` constant. Implement `IConfigListener` to react
|
||||
to changes.
|
||||
- Every user-visible string goes through `LangUtil.translate("...")`.
|
||||
- Icons come from `IconLoader`.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
mvn -q -pl diylc-swing -am compile
|
||||
```
|
||||
|
||||
Compilation is not evidence that the UI works. Say so, and offer to walk through launching the app
|
||||
via `.run/DIYLCStarter.run.xml` (which carries the required `--add-opens` / `--add-exports` flags) for
|
||||
a visual check.
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: regression-test
|
||||
description: Run the DIYLC end-to-end regression suite, which renders ~700 sample .diy projects and compares PNG exports and netlists against reference outputs. Use after any change to rendering, component drawing, serialization, netlist or analyzer logic, or when asked to check for regressions or regenerate reference files.
|
||||
---
|
||||
|
||||
# Running the DIYLC regression suite
|
||||
|
||||
`RegressionTestRunner` (in `diylc-swing/src/test/java/org/diylc/`) loads every sample project under
|
||||
`diylc-regression-data/input`, exports a PNG and extracts netlists, and compares both against
|
||||
reference files produced by a known-good build. It is the only automated coverage of component
|
||||
appearance and of the file format end to end — unit tests do not touch either.
|
||||
|
||||
**Always ask before running the full suite**, and never run `PREPARE` without explicit instruction.
|
||||
|
||||
## Modes
|
||||
|
||||
- **`TEST`** — compares against the reference files and writes a CSV report plus visual diff images.
|
||||
This is what you almost always want.
|
||||
- **`PREPARE`** — *regenerates* the reference PNGs and netlists from the current build. This
|
||||
overwrites the golden copy and is correct only once a human has inspected the differences and
|
||||
decided they are intended. Never run it on your own initiative.
|
||||
|
||||
## Easiest path
|
||||
|
||||
If the maintainer is at their IDE, the `.run/RegressionTestRunner.run.xml` IntelliJ configuration
|
||||
already has the arguments and JVM flags. Offer that first.
|
||||
|
||||
## From the terminal
|
||||
|
||||
The runner needs the `--add-opens` / `--add-exports` flags and a classpath spanning all three
|
||||
modules; `mvn exec:java` will not work because it runs in Maven's own classloader and fails to find
|
||||
Reflections. Build a classpath and fork a JVM:
|
||||
|
||||
```bash
|
||||
cd diylc
|
||||
SP=/tmp # or the session scratchpad
|
||||
|
||||
mvn -q -pl diylc-swing -am test-compile
|
||||
mvn -q -pl diylc-swing dependency:build-classpath -Dmdep.outputFile=$SP/cp-swing.txt -Dmdep.includeScope=test
|
||||
mvn -q -pl diylc-core dependency:build-classpath -Dmdep.outputFile=$SP/cp-core.txt -Dmdep.includeScope=test
|
||||
mvn -q -pl diylc-library dependency:build-classpath -Dmdep.outputFile=$SP/cp-lib.txt -Dmdep.includeScope=test
|
||||
|
||||
java \
|
||||
--add-exports java.desktop/com.apple.eawt.event=ALL-UNNAMED \
|
||||
--add-exports java.desktop/com.apple.eio=ALL-UNNAMED \
|
||||
--add-opens java.base/java.util=ALL-UNNAMED \
|
||||
--add-opens java.base/java.lang=ALL-UNNAMED \
|
||||
--add-opens java.base/java.text=ALL-UNNAMED \
|
||||
--add-opens java.desktop/java.awt=ALL-UNNAMED \
|
||||
--add-opens java.desktop/java.awt.font=ALL-UNNAMED \
|
||||
--add-opens java.desktop/java.awt.geom=ALL-UNNAMED \
|
||||
-cp "diylc-swing/target/test-classes:diylc-swing/target/classes:diylc-core/target/classes:diylc-library/target/classes:$(cat $SP/cp-swing.txt):$(cat $SP/cp-core.txt):$(cat $SP/cp-lib.txt)" \
|
||||
org.diylc.RegressionTestRunner \
|
||||
"$(cd .. && pwd)/diylc-regression-data" TEST [filter]
|
||||
```
|
||||
|
||||
Sanity-check the log line `Presenter - Current DIYLC version: 6.4.0` near the end. If it reports
|
||||
`5.0.0`, the `--add-opens` flags are missing and the run is invalid.
|
||||
|
||||
### Arguments
|
||||
|
||||
1. Path to `diylc-regression-data`
|
||||
2. `TEST` or `PREPARE`
|
||||
3. Optional filter — a substring matched against file paths. **Use it.** The full suite is ~700
|
||||
projects; while narrowing down an issue, run one file or one directory at a time
|
||||
(e.g. `splitter2`, or `guitar`).
|
||||
|
||||
## Reading the results
|
||||
|
||||
- A CSV report is written to `diylc-regression-data/reports/`, named with the timestamp and version,
|
||||
listing each file as OK or failed with the reason (`Images do not match`, `Netlists do not match
|
||||
with switches`, `Netlists do not match without switches`).
|
||||
- Rendered output and **visual diff images**, with red rectangles around every differing region, go
|
||||
to `diylc-regression-data/output/` (not under version control).
|
||||
- Reports are kept as historical evidence. If a run was exploratory rather than a real verification,
|
||||
delete the generated CSV afterwards so the reports directory stays meaningful — and check
|
||||
`git status` before finishing, since the runner writes into the repo.
|
||||
|
||||
## Interpreting a failure
|
||||
|
||||
A difference is not automatically a bug, but it is always a change the maintainer must see. Report
|
||||
which files differ and what kind of difference it is; where an image diff exists, point at the file
|
||||
so it can be looked at. Do not conclude "no regressions" from a filtered run — say exactly which
|
||||
subset was run.
|
||||
@@ -147,3 +147,10 @@ buildNumber.properties
|
||||
|
||||
# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
|
||||
!/.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
######################
|
||||
###### Custom ######
|
||||
######################
|
||||
|
||||
# Deployment folder
|
||||
deploy/
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DIYLCStarter" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="temurin-21" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
||||
<option name="MAIN_CLASS_NAME" value="org.diylc.DIYLCStarter" />
|
||||
<module name="diylc-swing" />
|
||||
<option name="VM_PARAMETERS" value="-Xms1024m -Xmx4096m -Ddorg.diylc.debugAreaPerformance=true -Dorg.diylc.scriptRun=true -Dfile.encoding=UTF-8 --add-exports java.desktop/com.apple.eawt.event=ALL-UNNAMED --add-exports java.desktop/com.apple.eio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.desktop/java.awt.geom=ALL-UNNAMED" />
|
||||
<option name="VM_PARAMETERS" value="-Xms1024m -Xmx4096m -Ddorg.diylc.debugAreaPerformance=true -Dorg.diylc.scriptRun=true -Ddorg.diylc.cloud.baseUrl=http://localhost:8080 -Dfile.encoding=UTF-8 --add-exports java.desktop/com.apple.eawt.event=ALL-UNNAMED --add-exports java.desktop/com.apple.eio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.desktop/java.awt.geom=ALL-UNNAMED" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="org.diylc.*" />
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RegressionTestRunner" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="MAIN_CLASS_NAME" value="org.diylc.RegressionTestRunner" />
|
||||
<module name="diylc-swing" />
|
||||
<option name="PROGRAM_PARAMETERS" value="$PROJECT_DIR$/../diylc-regression-data TEST" />
|
||||
<option name="VM_PARAMETERS" value="--add-exports java.desktop/com.apple.eawt.event=ALL-UNNAMED --add-exports java.desktop/com.apple.eio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.desktop/java.awt.geom=ALL-UNNAMED" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="org.diylc.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
CLAUDE.md
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
# DIY Layout Creator — agent guidelines
|
||||
|
||||
DIYLC is a Java 21 Swing desktop application for drawing circuit layouts, schematics and guitar
|
||||
wiring diagrams. It is a mature codebase (~690 source files) with long-standing conventions and a
|
||||
file format that must stay readable by every version. **Consistency with the surrounding code
|
||||
outweighs personal preference or modern idiom in every case.**
|
||||
|
||||
## The one rule that matters most
|
||||
|
||||
Before writing anything, read two or three existing files that do the same kind of job — the
|
||||
neighbouring component, the sibling plugin, the analyzer next to the one you are touching — and
|
||||
match them. Match their naming, their structure, their comment density, their ordering of members,
|
||||
their choice of helper classes. If your change looks different from its neighbours, it is wrong even
|
||||
if it compiles and works.
|
||||
|
||||
## Modules
|
||||
|
||||
Maven reactor rooted at `diylc/pom.xml`, version `6.4.0`, three modules with a strict one-way
|
||||
dependency chain:
|
||||
|
||||
| Module | Contains | Depends on |
|
||||
| --- | --- | --- |
|
||||
| `diylc-core` | Domain model, `Presenter`, rendering pipeline, messaging, serialization, netlist engine, annotations | — |
|
||||
| `diylc-library` | Concrete component classes, transformers, datasheets, legacy file parsers, component-aware analyzers | `diylc-core` |
|
||||
| `diylc-swing` | Swing shell, plugins, dialogs, editors, `DIYLCStarter` | `diylc-core`, `diylc-library` |
|
||||
|
||||
**`diylc-core` must never reference a concrete component type.** It knows components only through
|
||||
`IDIYComponent` and the annotations. If core logic seems to need knowledge of a specific component,
|
||||
that knowledge belongs in `diylc-library` (an analyzer, a transformer, a parser) or is exposed
|
||||
through a new interface in core that the component implements — look at
|
||||
`org.diylc.core.gerber.IGerberComponentSimple` for the established shape of that escape hatch.
|
||||
|
||||
Component and plugin classes are discovered at runtime by Reflections scanning the `org.diylc`
|
||||
package (`ComponentProcessor`). There is no registry to update when adding a component.
|
||||
|
||||
## Build and verify
|
||||
|
||||
Run from `diylc/`. Before reporting a change as done:
|
||||
|
||||
```bash
|
||||
mvn -q -pl diylc-core -am compile # after core changes
|
||||
mvn -q -pl diylc-library -am test # after library changes (~20s)
|
||||
mvn -q -pl diylc-swing -am compile # after swing changes
|
||||
mvn -q test # whole reactor, when the change spans modules
|
||||
```
|
||||
|
||||
Compile the module you touched plus everything downstream of it, and run the unit tests of any
|
||||
module whose tests could plausibly cover the change. Do not report success without having actually
|
||||
run these — quote the failure if something breaks rather than describing it.
|
||||
|
||||
Do not run `mvn package`, `build-all-profiles.sh`, or the regression suite unless asked; they are
|
||||
slow and the deployment build needs Docker and signing certificates. The regression suite is
|
||||
described in `.claude/skills/regression-test/SKILL.md` and is worth *offering* whenever a change
|
||||
affects rendering, serialization or netlist output.
|
||||
|
||||
The app runs from the `.run/DIYLCStarter.run.xml` IntelliJ configuration, which carries the required
|
||||
`--add-opens`/`--add-exports` JVM flags. Copy them if launching from a terminal.
|
||||
|
||||
## Code style
|
||||
|
||||
The formatter is `eclipse-java-google-style.xml` (Google Java Style):
|
||||
|
||||
- **Two-space indent**, four spaces for continuation lines. No tabs.
|
||||
- **100-column** line limit.
|
||||
- Source encoding is **ISO-8859-1** — do not introduce non-ASCII characters into `.java` files. Use
|
||||
Unicode escapes or the resource bundles instead.
|
||||
- Braces on the same line, and **always braces** — an `if`, `else`, `for` or `while` body gets a
|
||||
block even when it is a single statement. Plenty of older code omits them; do not copy that when
|
||||
you are writing new code, and add them when you are already editing the lines in question.
|
||||
- Imports: `java.*` first, then third-party, then `org.diylc.*`, separated by blank lines. No
|
||||
wildcard imports.
|
||||
- Constants are `UPPER_SNAKE_CASE`. Note that component appearance constants are deliberately
|
||||
`public static` and **not** `final` (`BODY_COLOR`, `DEFAULT_WIDTH`), so they can be overridden at
|
||||
runtime; keep that convention when adding new ones.
|
||||
- `private static final Logger LOG = Logger.getLogger(TheClass.class);` — log4j 1.2, declared first
|
||||
among the static members. Only declare one on a class that actually needs it; see *Logging* below.
|
||||
- **Nested types go at the bottom of the file**, after every field, constructor and method of the
|
||||
enclosing class, immediately before its closing brace. A version enum or a small helper class
|
||||
declared halfway down the file pushes the code a reader came for out of sight; keeping them last
|
||||
means the outer class reads top to bottom without interruption. Several nested types keep their
|
||||
relative order.
|
||||
|
||||
### Comments
|
||||
|
||||
Write comments only where the code cannot speak for itself: a non-obvious electrical or geometric
|
||||
reason, a workaround for a library bug, a compatibility constraint, a unit or coordinate-system
|
||||
assumption. Never restate the code.
|
||||
|
||||
```java
|
||||
// BAD — adds nothing
|
||||
// set the body color
|
||||
g2d.setColor(bodyColor);
|
||||
|
||||
// GOOD — explains something the reader cannot infer
|
||||
// the outer foil marking is drawn on the lead that is closer to the first control point,
|
||||
// which is the convention printed on most film capacitors
|
||||
```
|
||||
|
||||
Javadoc on public interfaces and abstract base classes in `diylc-core` is expected and should carry
|
||||
`@author`. Concrete component classes generally carry no Javadoc — do not add it just to fill space.
|
||||
|
||||
### Logging
|
||||
|
||||
Keep logging sparse, and prefer removing a log line to adding one. A logger earns its place on a
|
||||
class that has something a maintainer would want to read back: a swallowed failure, a slow or
|
||||
one-off initialisation, an external file or service that may not be there. Do not declare a `LOG`
|
||||
field that the class never uses.
|
||||
|
||||
- `LOG.error` — something went wrong that the user or a bug report may need to explain.
|
||||
- `LOG.warn` — a recoverable surprise.
|
||||
- `LOG.info` — events that happen a handful of times per session, not per action.
|
||||
- `LOG.debug` — anything finer. Assume it is switched off in the field.
|
||||
|
||||
Never log from code on a hot path: `draw`, control-point accessors, property getters, anything the
|
||||
canvas calls on every repaint or every mouse move. The cost is real and the output buries whatever
|
||||
else is in the log.
|
||||
|
||||
Do not wrap code in `try`/`catch` only so that there is something to log. If the block cannot
|
||||
actually throw, the guard and the logger both come out.
|
||||
|
||||
### License header
|
||||
|
||||
**Every new source file starts with the GPL header**, copied verbatim from `HEADER.txt` (or from any
|
||||
existing class), immediately followed by the `package` declaration with no blank line between the
|
||||
closing `*/` and `package`. This applies to Java files in all three modules, including tests.
|
||||
|
||||
## File-format backward compatibility
|
||||
|
||||
DIYLC projects are XStream-serialized object graphs. Users open decade-old `.diy` files with the
|
||||
current build, so **the serialized shape of a component class is a public contract**.
|
||||
|
||||
- Never rename or delete a serialized field, and never change its type. To replace one, keep the old
|
||||
field, add a new one, mark the old accessors `@Deprecated`, and migrate lazily inside the new
|
||||
getter. The canonical examples are `AbstractTransparentComponent.getAlpha()` (legacy `Byte alpha`
|
||||
→ `Percentage alphaPercent`) and `Resistor.getPowerNew()` / `AbstractFilmCapacitor.getVoltageNew()`.
|
||||
- Fields added to an existing class deserialize as `null` in old files. Every read path must tolerate
|
||||
that, typically by defaulting in the getter rather than the constructor.
|
||||
- Never rename or move a component class that has shipped. If a component genuinely must change
|
||||
identity, an alias has to be registered on the XStream side and the legacy parsers in
|
||||
`V1FileParser` / `V2FileParser` reviewed.
|
||||
- Prefer **a `Version` property on a single component class over a new class per revision** when a
|
||||
board or part gains a new variant. One class with a version enum keeps old files loading and keeps
|
||||
the toolbox uncluttered.
|
||||
- Any change here calls for a regression-suite run.
|
||||
|
||||
## Components
|
||||
|
||||
See `.claude/skills/add-component/SKILL.md` for the full recipe. The rules that apply to *editing*
|
||||
existing components too:
|
||||
|
||||
- Extend the most specific abstract base that fits (`AbstractLeadedComponent`,
|
||||
`AbstractRadialComponent`, `AbstractBoard`, `AbstractTransparentComponent`, …) rather than
|
||||
reimplementing shared behaviour.
|
||||
- Every editable property needs a matching getter/setter pair with `@EditableProperty` on the
|
||||
**getter**; the property name is derived from the getter unless `name` is given.
|
||||
- Drawing code must fetch strokes from `ObjectCache.getInstance().fetchBasicStroke(...)` /
|
||||
`fetchZoomableStroke(...)` / `fetchStroke(...)` rather than allocating `new BasicStroke(...)`.
|
||||
`draw()` runs on every repaint; do not allocate what can be cached or reused.
|
||||
- Conductive areas are reported to the `IDrawingObserver` via
|
||||
`startTrackingContinuityArea(true)` / `stopTrackingContinuityArea()`. Getting this wrong silently
|
||||
breaks continuity checks and the netlist, so mirror a neighbouring component exactly.
|
||||
- Rotation and mirroring live in an `IComponentTransformer` referenced from `@ComponentDescriptor`,
|
||||
never inside the component.
|
||||
- `drawIcon()` draws into a small fixed box for the toolbox; it is hand-drawn, not scaled from
|
||||
`draw()`.
|
||||
|
||||
## Plugins and Swing
|
||||
|
||||
- Everything in the UI is an `IPlugIn` installed from `MainFrame` via `presenter.installPlugin(...)`.
|
||||
- Plugins talk to the domain only through `IPlugInPort`, and react to domain changes by returning a
|
||||
narrow `EnumSet<EventType>` from `getSubscribedEventTypes()`. Subscribe to as little as possible —
|
||||
`PROJECT_MODIFIED` fires constantly.
|
||||
- UI is injected through `ISwingUI.injectMenuAction` / `injectGUIComponent` / `injectSubmenu`, not by
|
||||
reaching into frames directly.
|
||||
- Persisted settings go through `ConfigurationManager.getInstance().readString/writeValue` with a
|
||||
`String` key constant.
|
||||
- User-visible strings are wrapped in `LangUtil.translate(...)`.
|
||||
- Long-running work belongs on a background executor, never on the EDT.
|
||||
|
||||
## Testing
|
||||
|
||||
JUnit 4 under `src/test/java` in each module. Add tests where the project already tests: switching
|
||||
logic, netlist and analyzer behaviour, datasheet parsing, core utilities. Component rendering and
|
||||
user interaction are covered by the regression suite instead, not by unit tests. Test classes are
|
||||
named `*Test` or `*Tests` and carry the license header like any other file.
|
||||
|
||||
## Git
|
||||
|
||||
**Do not commit or push.** Make the change in the working tree and stop; the maintainer reviews and
|
||||
commits. If a commit is explicitly requested, commit only what was asked for, never to a remote, and
|
||||
never `git add -A` over unrelated dirty files.
|
||||
|
||||
## Out of scope unless explicitly asked
|
||||
|
||||
- `diylc-server-api/` — PHP backend that runs in an isolated environment on diy-fever.com and cannot
|
||||
be tested locally.
|
||||
- `deploy/`, `build-all-profiles.sh`, packaging and notarization.
|
||||
- `lib/` — vendored jars for dependencies not on Maven Central.
|
||||
- `diylc-regression-data/reports/` — historical evidence, append-only.
|
||||
- Bumping the project version in the POMs.
|
||||
|
||||
## Task recipes
|
||||
|
||||
Detailed, step-by-step recipes live in `.claude/skills/` and are worth reading in full before
|
||||
starting the corresponding kind of work:
|
||||
|
||||
| Skill | Covers |
|
||||
| --- | --- |
|
||||
| `add-component` | Creating or substantially changing a component in `diylc-library` |
|
||||
| `add-plugin` | Adding UI: plugins, menu actions, panels, dialogs, background tasks |
|
||||
| `regression-test` | Running the end-to-end regression suite and reading its report |
|
||||
|
||||
## Reference
|
||||
|
||||
Chapter 13 of *Mastering DIY Layout Creator* is the authoritative developer guide and covers the
|
||||
architecture, extension points and regression suite in prose.
|
||||
Executable
+161
@@ -0,0 +1,161 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to build all Maven profiles and copy zip files to deployment folder
|
||||
# Usage: ./build-all-profiles.sh [to-deploy-folder]
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Parse arguments
|
||||
DEPLOY_DIR="${1:-}"
|
||||
|
||||
# Configuration
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SWING_MODULE_DIR="${PROJECT_DIR}/diylc-swing"
|
||||
DEPLOY_DIR="${DEPLOY_DIR:-${PROJECT_DIR}/deploy}"
|
||||
|
||||
# Docker must be up before jpackage-based profiles can run
|
||||
ensure_docker_running() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "ERROR: docker is not installed or not on PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if docker info &> /dev/null; then
|
||||
echo "Docker is already running"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Docker is not running, attempting to start it..."
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
open -a Docker
|
||||
elif command -v systemctl &> /dev/null; then
|
||||
sudo systemctl start docker
|
||||
else
|
||||
echo "ERROR: Don't know how to start Docker on this platform"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# the daemon keeps refusing connections for a while after the app itself launches
|
||||
for _ in $(seq 1 60); do
|
||||
if docker info &> /dev/null; then
|
||||
echo "Docker is running"
|
||||
return
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "ERROR: Timed out waiting for Docker to start"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# List of profiles to build
|
||||
PROFILES=(
|
||||
"universal-zip"
|
||||
"linux-appimage"
|
||||
"win64-zip"
|
||||
"win64-installer"
|
||||
"mac-x86"
|
||||
"mac-arm"
|
||||
)
|
||||
|
||||
echo "=========================================="
|
||||
echo "Building all Maven profiles"
|
||||
echo "=========================================="
|
||||
echo "Project directory: ${PROJECT_DIR}"
|
||||
echo "Swing module directory: ${SWING_MODULE_DIR}"
|
||||
echo "Deployment directory: ${DEPLOY_DIR}"
|
||||
echo "Tests: Skipped"
|
||||
echo ""
|
||||
|
||||
ensure_docker_running
|
||||
echo ""
|
||||
|
||||
# Create deployment directory if it doesn't exist
|
||||
mkdir -p "${DEPLOY_DIR}"
|
||||
|
||||
# Change to project root directory
|
||||
cd "${PROJECT_DIR}"
|
||||
|
||||
# Build each profile (clean before each one)
|
||||
TARGET_DIR="${SWING_MODULE_DIR}/target"
|
||||
TOTAL_ZIP_COUNT=0
|
||||
|
||||
for profile in "${PROFILES[@]}"; do
|
||||
echo "----------------------------------------"
|
||||
echo "Building profile: ${profile}"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# Clean and build from root, specifying the swing module and profile
|
||||
# This ensures dependencies (diylc-core, diylc-library) are built first
|
||||
mvn clean package -pl diylc-swing -am -P"${profile}" -DskipTests || {
|
||||
echo "ERROR: Failed to build profile ${profile}"
|
||||
echo "Continuing with next profile..."
|
||||
continue
|
||||
}
|
||||
|
||||
echo "✓ Profile ${profile} built successfully"
|
||||
|
||||
# Copy zip and exe files immediately after each build (before next clean)
|
||||
if [ -d "${TARGET_DIR}" ]; then
|
||||
echo "Looking for artifacts in ${TARGET_DIR}..."
|
||||
PROFILE_FILE_COUNT=0
|
||||
|
||||
# Find all zip files in target directory (excluding DIYLC-notarize.zip)
|
||||
while IFS= read -r -d '' zipfile; do
|
||||
filename=$(basename "${zipfile}")
|
||||
# Skip DIYLC-notarize.zip files
|
||||
if [[ "${filename}" != "DIYLC-notarize.zip" ]] && [ -f "${zipfile}" ]; then
|
||||
dest="${DEPLOY_DIR}/${filename}"
|
||||
|
||||
echo " → Copying: ${filename}"
|
||||
cp "${zipfile}" "${dest}"
|
||||
TOTAL_ZIP_COUNT=$((TOTAL_ZIP_COUNT + 1))
|
||||
PROFILE_FILE_COUNT=$((PROFILE_FILE_COUNT + 1))
|
||||
fi
|
||||
done < <(find "${TARGET_DIR}" -maxdepth 1 -type f -name "*.zip" -print0 2>/dev/null)
|
||||
|
||||
# Find all exe files in target directory
|
||||
while IFS= read -r -d '' exefile; do
|
||||
if [ -f "${exefile}" ]; then
|
||||
filename=$(basename "${exefile}")
|
||||
dest="${DEPLOY_DIR}/${filename}"
|
||||
|
||||
echo " → Copying: ${filename}"
|
||||
cp "${exefile}" "${dest}"
|
||||
TOTAL_ZIP_COUNT=$((TOTAL_ZIP_COUNT + 1))
|
||||
PROFILE_FILE_COUNT=$((PROFILE_FILE_COUNT + 1))
|
||||
fi
|
||||
done < <(find "${TARGET_DIR}" -maxdepth 1 -type f -name "*.exe" -print0 2>/dev/null)
|
||||
|
||||
if [ ${PROFILE_FILE_COUNT} -eq 0 ]; then
|
||||
echo " ⚠ Warning: No artifacts found for profile ${profile}"
|
||||
# List what's actually in the target directory for debugging
|
||||
echo " Debug: Files in target directory:"
|
||||
ls -la "${TARGET_DIR}"/*.{zip,exe} 2>/dev/null | head -5 || echo " (no zip/exe files found)"
|
||||
else
|
||||
echo " ✓ Copied ${PROFILE_FILE_COUNT} file(s) from ${profile}"
|
||||
fi
|
||||
else
|
||||
echo " ⚠ Warning: Target directory ${TARGET_DIR} does not exist"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "=========================================="
|
||||
echo "Copying summary"
|
||||
echo "=========================================="
|
||||
if [ ${TOTAL_ZIP_COUNT} -eq 0 ]; then
|
||||
echo "No artifacts were copied"
|
||||
else
|
||||
echo "✓ Copied ${TOTAL_ZIP_COUNT} file(s) total to ${DEPLOY_DIR}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Build and deployment complete!"
|
||||
echo "=========================================="
|
||||
echo "Deployment folder: ${DEPLOY_DIR}"
|
||||
echo "Files:"
|
||||
ls -lh "${DEPLOY_DIR}"/*.{zip,exe} 2>/dev/null || echo " (no zip/exe files found)"
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
<parent>
|
||||
<groupId>org.diylc</groupId>
|
||||
<artifactId>diylc-parent</artifactId>
|
||||
<version>5.7.0</version>
|
||||
<version>6.7.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>diylc-core</artifactId>
|
||||
<!-- No need to specify version - it inherits from parent -->
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -60,7 +60,7 @@
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.16.1</version>
|
||||
<version>2.22.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
@@ -74,6 +74,17 @@
|
||||
<version>5.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- Subclass mock maker, selected in src/test/resources/mockito-extensions. Mockito 5
|
||||
defaults to the inline maker, which attaches a Java agent to the running JVM; that
|
||||
attachment fails outright on some CI JDKs and is deprecated since Java 21. Every type
|
||||
mocked in these tests is an interface, which the subclass maker handles without an
|
||||
agent. -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-subclass</artifactId>
|
||||
<version>5.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Local dependencies from lib folder -->
|
||||
<dependency>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.announcements;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utility methods for detecting and extracting URLs from announcements and HTML.
|
||||
*/
|
||||
public class AnnouncementUtils {
|
||||
|
||||
private static final Pattern HREF_PATTERN = Pattern.compile(
|
||||
"href=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* Extracts the first URL found in the text or HTML.
|
||||
*
|
||||
* @param html text or HTML content
|
||||
* @return first URL found, or null if none
|
||||
*/
|
||||
public static String extractFirstUrl(String html) {
|
||||
List<String> urls = extractUrls(html);
|
||||
return urls.isEmpty() ? null : urls.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all unique URLs found in the HTML href attributes in order of appearance.
|
||||
*
|
||||
* @param html text or HTML content
|
||||
* @return list of unique URLs
|
||||
*/
|
||||
public static List<String> extractUrls(String html) {
|
||||
List<String> urls = new ArrayList<String>();
|
||||
if (html == null || html.isEmpty()) {
|
||||
return urls;
|
||||
}
|
||||
|
||||
Matcher hrefMatcher = HREF_PATTERN.matcher(html);
|
||||
while (hrefMatcher.find()) {
|
||||
String url = hrefMatcher.group(1).trim();
|
||||
url = normalizeUrl(url);
|
||||
if (!urls.contains(url)) {
|
||||
urls.add(url);
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the HTML contains any link.
|
||||
*
|
||||
* @param html text or HTML content
|
||||
* @return true if at least one URL is detected, false otherwise
|
||||
*/
|
||||
public static boolean containsLink(String html) {
|
||||
return extractFirstUrl(html) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a URL to ensure it has a valid protocol if starting with www.
|
||||
*/
|
||||
public static String normalizeUrl(String url) {
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = url.trim();
|
||||
if (trimmed.startsWith("www.")) {
|
||||
return "http://" + trimmed;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.diylc.core.ComponentGroup;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
@@ -44,19 +45,21 @@ public class ComponentTransferable implements Transferable, Serializable {
|
||||
public static final DataFlavor listFlavor = new DataFlavor(ComponentTransferable.class, "application/diylc");
|
||||
|
||||
private List<IDIYComponent<?>> components;
|
||||
private Set<Set<IDIYComponent<?>>> groups;
|
||||
private Set<ComponentGroup> groups;
|
||||
private String name;
|
||||
|
||||
public ComponentTransferable() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ComponentTransferable(List<IDIYComponent<?>> components) {
|
||||
public ComponentTransferable(List<IDIYComponent<?>> components, String name) {
|
||||
super();
|
||||
this.components = components;
|
||||
this.name = name;
|
||||
this.groups = null;
|
||||
}
|
||||
|
||||
public ComponentTransferable(List<IDIYComponent<?>> components, Set<Set<IDIYComponent<?>>> groups) {
|
||||
public ComponentTransferable(List<IDIYComponent<?>> components, Set<ComponentGroup> groups) {
|
||||
super();
|
||||
this.components = components;
|
||||
this.groups = groups;
|
||||
@@ -84,7 +87,11 @@ public class ComponentTransferable implements Transferable, Serializable {
|
||||
return components;
|
||||
}
|
||||
|
||||
public Set<Set<IDIYComponent<?>>> getGroups() {
|
||||
public Set<ComponentGroup> getGroups() {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
+23
-25
@@ -17,15 +17,11 @@
|
||||
*/
|
||||
package org.diylc.clipboard;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import org.diylc.core.ComponentGroup;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
public class ComponentTransferableFactory {
|
||||
@@ -40,13 +36,19 @@ public class ComponentTransferableFactory {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ComponentTransferable build(Collection<IDIYComponent<?>> selectedComponents, Set<Set<IDIYComponent<?>>> groups) {
|
||||
Set<IDIYComponent<?>> originalComponentSet = new HashSet<IDIYComponent<?>>(selectedComponents);
|
||||
public ComponentTransferable build(Collection<IDIYComponent<?>> selectedComponents, Set<ComponentGroup> groups) {
|
||||
|
||||
List<IDIYComponent<?>> originalComponents = new ArrayList<IDIYComponent<?>>(selectedComponents);
|
||||
|
||||
Map<UUID, UUID> idMap = new HashMap<>();
|
||||
|
||||
List<IDIYComponent<?>> clonedComponents = originalComponents.stream(
|
||||
).map(x -> {
|
||||
try {
|
||||
return x.clone();
|
||||
IDIYComponent<?> clone = x.clone();
|
||||
clone.setId(UUID.randomUUID());
|
||||
idMap.put(x.getId(), clone.getId());
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
LOG.error(e);
|
||||
return null;
|
||||
@@ -54,22 +56,18 @@ public class ComponentTransferableFactory {
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
Set<Set<IDIYComponent<?>>> clonedGroups = new HashSet<Set<IDIYComponent<?>>>();
|
||||
|
||||
for (Set<IDIYComponent<?>> group : groups) {
|
||||
if (group.isEmpty())
|
||||
continue;
|
||||
|
||||
if (originalComponentSet.contains(group.iterator().next())) {
|
||||
Set<IDIYComponent<?>> clonedGroup = new HashSet<IDIYComponent<?>>();
|
||||
clonedGroups.add(clonedGroup);
|
||||
for (int i = 0; i < originalComponents.size(); i++) {
|
||||
if (group.contains(originalComponents.get(i))) {
|
||||
clonedGroup.add(clonedComponents.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<ComponentGroup> clonedGroups = groups.stream()
|
||||
.map(group -> {
|
||||
Set<UUID> newIds = group.getComponentIds().stream()
|
||||
.map(idMap::get)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
// Only create group if it has at least one valid ID
|
||||
return newIds.isEmpty() ? null : new ComponentGroup(newIds, group.getName());
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return new ComponentTransferable(clonedComponents, clonedGroups);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.common;
|
||||
|
||||
/**
|
||||
* The two ways a saved {@link BuildingBlock} can be placed on the canvas.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public enum BlockInstantiationMode {
|
||||
|
||||
/**
|
||||
* The block's components are cloned as individual, independently selectable components and
|
||||
* grouped together (the original, and only, behavior before composite mode was introduced).
|
||||
*/
|
||||
GROUP,
|
||||
|
||||
/**
|
||||
* The block's components are cloned and wrapped in a single
|
||||
* {@code org.diylc.components.composite.CompositeComponent} that behaves as one rigid
|
||||
* component with its own name, aggregating the control points of all its children.
|
||||
*/
|
||||
COMPOSITE
|
||||
}
|
||||
@@ -56,14 +56,14 @@ public class ComponentType implements Serializable {
|
||||
private boolean enableCache;
|
||||
private List<String[]> datasheet;
|
||||
private int datasheetCreationStepCount;
|
||||
|
||||
public ComponentType() {
|
||||
|
||||
public ComponentType() {
|
||||
}
|
||||
|
||||
public ComponentType(String name, String description, CreationMethod creationMethod, String category,
|
||||
String namePrefix, String author, Icon icon, Class<? extends IDIYComponent<?>> instanceClass, double zOrder,
|
||||
boolean flexibleZOrder, BomPolicy bomPolicy, boolean autoEdit, IComponentTransformer transformer,
|
||||
KeywordPolicy keywordPolicy, String keywordTag, boolean enableCache, List<String[]> datasheet,
|
||||
boolean flexibleZOrder, BomPolicy bomPolicy, boolean autoEdit, IComponentTransformer transformer,
|
||||
KeywordPolicy keywordPolicy, String keywordTag, boolean enableCache, List<String[]> datasheet,
|
||||
int datasheetCreationStepCount) {
|
||||
super();
|
||||
this.name = name;
|
||||
|
||||
@@ -68,5 +68,9 @@ public enum DrawOption {
|
||||
/**
|
||||
* Whether locked components should be rendered as transparent or not.
|
||||
*/
|
||||
LOCKED_ALPHA;
|
||||
LOCKED_ALPHA,
|
||||
/**
|
||||
* When applied, all colors will be converted to blackest black or whitest white
|
||||
*/
|
||||
MONOCHROMATIC;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
*/
|
||||
package org.diylc.common;
|
||||
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.core.Project;
|
||||
@@ -55,12 +54,7 @@ public enum EventType {
|
||||
*/
|
||||
FILE_STATUS_CHANGED,
|
||||
/**
|
||||
* Called when selection rectangle is changed. Object of type {@link Rectangle} is the only
|
||||
* parameter and it contains the new selection rectangle.
|
||||
*/
|
||||
SELECTION_RECT_CHANGED,
|
||||
/**
|
||||
* Called when component selection is changed. New {@link ComponentSelection} is attached as a
|
||||
* Called when component selection is changed. New list of components and control point map is attached as a
|
||||
* parameter.
|
||||
*/
|
||||
SELECTION_CHANGED,
|
||||
@@ -118,5 +112,16 @@ public enum EventType {
|
||||
/**
|
||||
* Signals that the Cloud account is logged out.
|
||||
*/
|
||||
CLOUD_LOGGED_OUT;
|
||||
CLOUD_LOGGED_OUT,
|
||||
/**
|
||||
* Called when the mouse hovers over sticky control points (nodes) in tooltip mode.
|
||||
* Parameters: String text (semicolon-separated component and node names, or null), Point point.
|
||||
*/
|
||||
NODE_NAME_HOVER_TOOLTIP,
|
||||
/**
|
||||
* Called when the set of enabled component categories changes. The first parameter is a
|
||||
* <code>Set<String></code> of disabled category names. All categories not included are
|
||||
* enabled.
|
||||
*/
|
||||
COMPONENT_CATEGORIES_CHANGED;
|
||||
}
|
||||
|
||||
@@ -25,13 +25,28 @@ import java.io.IOException;
|
||||
|
||||
public interface IBlockProcessor {
|
||||
|
||||
public static final String BLOCKS_KEY = "buildingBlocks";
|
||||
public static final String BLOCKS_KEY = "buildingBlocks";
|
||||
public static final String DEFAULT_BLOCKS_IMPORTED_KEY = "defaultBlocksImported";
|
||||
|
||||
/**
|
||||
* Prefix that marks a string as a reference to a building block rather than a component class
|
||||
* name. Used wherever components and blocks share a single identifier list, such as keyboard
|
||||
* shortcuts ({@code COMPONENT_SHORTCUT_KEY}) and the recently used list
|
||||
* ({@link org.diylc.common.IPlugInPort#RECENT_COMPONENTS_KEY}). A block identifier looks like
|
||||
* {@code "block:My Block Name"}.
|
||||
*/
|
||||
public static final String BLOCK_PREFIX = "block:";
|
||||
|
||||
void saveSelectionAsBlock(String blockName);
|
||||
|
||||
void loadBlock(String blockName) throws InvalidBlockException;
|
||||
|
||||
/**
|
||||
* Loads the block in the given {@link BlockInstantiationMode}. {@link BlockInstantiationMode#COMPOSITE}
|
||||
* (a single rigid component) is the default mode used by most call sites (see design decision D10 in
|
||||
* {@code docs/plans/composite-building-blocks.md}); {@link BlockInstantiationMode#GROUP} instantiates
|
||||
* the block as a group of loose components.
|
||||
*/
|
||||
void loadBlock(String blockName, BlockInstantiationMode mode) throws InvalidBlockException;
|
||||
|
||||
void deleteBlock(String blockName);
|
||||
|
||||
int importBlocks(String fileName) throws IOException;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.common;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.diylc.core.ComponentGroup;
|
||||
|
||||
/**
|
||||
* Interface for component grouping operations.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public interface IGroupProcessor {
|
||||
|
||||
/**
|
||||
* Returns the set of component groups in the current project. Each group is a set of components
|
||||
* that are grouped together.
|
||||
*
|
||||
* @return set of component groups
|
||||
*/
|
||||
Set<ComponentGroup> getComponentGroups();
|
||||
|
||||
/**
|
||||
* Groups all selected components.
|
||||
*/
|
||||
void groupSelectedComponents();
|
||||
|
||||
/**
|
||||
* Ungroups all selected components.
|
||||
*/
|
||||
void ungroupSelectedComponents();
|
||||
}
|
||||
|
||||
@@ -41,16 +41,18 @@ import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.Template;
|
||||
import org.diylc.core.Theme;
|
||||
import org.diylc.core.measures.ResizeDimensions;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.filter.IComponentFilter;
|
||||
import org.diylc.test.ITestProcessor;
|
||||
|
||||
/**
|
||||
* Interface for communication between plug-ins and the application. Plug-ins may acquire
|
||||
* information or make changes through this interface. Application events are dispatched to plug-ins
|
||||
* through {@link MessageDispatcher}
|
||||
*
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*
|
||||
*
|
||||
* @see IPlugIn
|
||||
* @see MessageDispatcher
|
||||
* @see IMessageListener
|
||||
@@ -81,21 +83,28 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
public static final String HARDWARE_ACCELERATION = "hardwareAcceleration";
|
||||
public static final String EXTRA_SPACE_KEY = "extraSpace";
|
||||
public static final String FAVORITES_KEY = "favorites";
|
||||
public static final String DISABLED_CATEGORIES_KEY = "disabledComponentCategories";
|
||||
public static final String RENUMBER_ON_PASTE_KEY = "renumberOnPaste";
|
||||
public static final String RULER_IN_SUBDIVISION_KEY = "rulerInSubdivision";
|
||||
public static final String CACHING_ENABLED_KEY = "cachingEnabled";
|
||||
|
||||
public static final String FONT_SCALE_FACTOR_KEY = "fontScaleFactor";
|
||||
public static final String SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY = "showResizeDimensionsTooltip";
|
||||
public static final String AUTO_UNIT_CONVERSION_KEY = "autoUnitConversion";
|
||||
|
||||
|
||||
public static final String RULER_IN_SUBDIVISION_2 = "base of 2";
|
||||
public static final String RULER_IN_SUBDIVISION_10 = "base of 10";
|
||||
public static final String RULER_IN_SUBDIVISION_DEFAULT = RULER_IN_SUBDIVISION_10;
|
||||
|
||||
|
||||
public static final String SNAP_TO_NONE = "None";
|
||||
public static final String SNAP_TO_GRID = "Grid";
|
||||
public static final String SNAP_TO_COMPONENTS = "Components";
|
||||
public static final String SNAP_TO_DEFAULT = SNAP_TO_GRID;
|
||||
|
||||
|
||||
public static final String SHOW_NODE_NAME_TOOLTIPS_KEY = "showNodeNameTooltips";
|
||||
|
||||
public static final String LOCKED_ALPHA = "lockedAlpha";
|
||||
|
||||
|
||||
public static final String LANGUAGE = "language";
|
||||
public static final String LANGUAGE_DEFAULT = "English";
|
||||
|
||||
@@ -108,25 +117,25 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
public static final int NOBUTTON = 0;
|
||||
public static final int BUTTON1 = 1;
|
||||
public static final int BUTTON2 = 2;
|
||||
public static final int BUTTON3 = 3;
|
||||
public static final int BUTTON3 = 3;
|
||||
|
||||
/**
|
||||
* Returns size of the canvas that takes project dimensions into account as well as zoom level.
|
||||
* Each dimension is calculated as the product of the actual size and the number of pixels per
|
||||
* unit. If <code>useZoom</code> is set to true, the result is scaled by zoom factor.
|
||||
*
|
||||
*
|
||||
* @param useZoom
|
||||
* @param includeExtraSpace
|
||||
*
|
||||
*
|
||||
* @return canvas dimensions
|
||||
*/
|
||||
Dimension getCanvasDimensions(boolean useZoom, boolean includeExtraSpace);
|
||||
|
||||
/**
|
||||
* Returns an instance of {@link Cursor} that should be used at the specified location.
|
||||
*
|
||||
*
|
||||
* Note: point coordinates are scaled for zoom factor.
|
||||
*
|
||||
*
|
||||
* @param point
|
||||
* @param ctrlDown
|
||||
* @param shiftDown
|
||||
@@ -137,14 +146,14 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
|
||||
/**
|
||||
* Returns an instance of currently loaded project.
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Project getCurrentProject();
|
||||
|
||||
/**
|
||||
* Loads specified {@link Project}.
|
||||
*
|
||||
*
|
||||
* @param project
|
||||
* @param freshStart
|
||||
* @param filename file name when loading from a file
|
||||
@@ -158,22 +167,22 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
|
||||
/**
|
||||
* Loads a project from the specified file.
|
||||
*
|
||||
*
|
||||
* @param fileName
|
||||
*/
|
||||
void loadProjectFromFile(String fileName);
|
||||
|
||||
/**
|
||||
* Saves the current project into the specified file.
|
||||
*
|
||||
*
|
||||
* @param fileName
|
||||
* @param isBackup
|
||||
*/
|
||||
void saveProjectToFile(String fileName, boolean isBackup);
|
||||
|
||||
|
||||
/**
|
||||
* Exports the current project into one or more files - one per layer and potentially drill layer.
|
||||
*
|
||||
*
|
||||
* @param fileNameBase
|
||||
* @param g2d
|
||||
*/
|
||||
@@ -191,7 +200,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
|
||||
/**
|
||||
* Shows a user dialog if there are changes to confirm that it's safe to proceed.
|
||||
*
|
||||
*
|
||||
* @return true, if file actions (new, open, close) can be taken
|
||||
*/
|
||||
boolean allowFileAction();
|
||||
@@ -200,15 +209,29 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
* Returns all available {@link ComponentType}s classified by category. Result is a {@link Map}
|
||||
* between category name to a {@link List} of all {@link ComponentType}s that share that category
|
||||
* name.
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Map<String, List<ComponentType>> getComponentTypes();
|
||||
|
||||
/**
|
||||
* Returns the {@link ComponentType}s that should be offered to the user, i.e. the result of
|
||||
* {@link #getComponentTypes()} without the categories the user disabled. Intended for component
|
||||
* browsers; anything that needs to resolve an arbitrary component class must use
|
||||
* {@link #getComponentTypes()} instead.
|
||||
*/
|
||||
Map<String, List<ComponentType>> getVisibleComponentTypes();
|
||||
|
||||
boolean isCategoryEnabled(String category);
|
||||
|
||||
void setCategoryEnabled(String category, boolean enabled);
|
||||
|
||||
void resetCategoryVisibility();
|
||||
|
||||
/**
|
||||
* Draws project on the provided {@link Graphics2D}. If the provided filter is not null, it will
|
||||
* be used to filter the components that are shown.
|
||||
*
|
||||
*
|
||||
* @param g2d
|
||||
* @param drawOptions specific drawing options
|
||||
* @param filter
|
||||
@@ -222,35 +245,35 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
|
||||
/**
|
||||
* Returns current zoom level where <code>zoomLevel = 1.0d</code> means 100%.
|
||||
*
|
||||
*
|
||||
* @return current zoom level
|
||||
*/
|
||||
double getZoomLevel();
|
||||
|
||||
/**
|
||||
* Changes current zoom level where <code>zoomLevel = 1.0d</code> means 100%
|
||||
*
|
||||
*
|
||||
* @param zoomLevel new zoom leve
|
||||
*/
|
||||
void setZoomLevel(double zoomLevel);
|
||||
|
||||
/**
|
||||
* Returns current version number.
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
VersionNumber getCurrentVersionNumber();
|
||||
|
||||
|
||||
/**
|
||||
* Returns few most recent versions.
|
||||
*
|
||||
* Returns few most recent versions.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
List<Version> getRecentUpdates();
|
||||
|
||||
/**
|
||||
* Adds a list of components to the project.
|
||||
*
|
||||
*
|
||||
* @param componentTransferable
|
||||
* @param autoGroup
|
||||
* @param assignNewNames
|
||||
@@ -269,7 +292,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
|
||||
/**
|
||||
* Sets default value for the specified property name for currently selected component types.
|
||||
*
|
||||
*
|
||||
* @param propertyName display name for property
|
||||
* @param value new default value, must not be null
|
||||
*/
|
||||
@@ -277,7 +300,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
|
||||
/**
|
||||
* Sets default value for the specified property name for projects.
|
||||
*
|
||||
*
|
||||
* @param clazz class to set defaults to
|
||||
* @param propertyName display name for property
|
||||
* @param value new default value, must not be null
|
||||
@@ -291,6 +314,15 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
||||
*/
|
||||
List<PropertyWrapper> getMutualSelectionProperties();
|
||||
|
||||
/**
|
||||
* Available only during resizing, returns the dimensions of the component being resized.
|
||||
* Otherwise returns null.
|
||||
*
|
||||
* @return ResizeDimensions with width, height (in mm or in depending on configuration),
|
||||
* and length (if component implements IHaveLength), or null if not resizing
|
||||
*/
|
||||
ResizeDimensions getResizeDimensions();
|
||||
|
||||
void editSelection();
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.diylc.core.ExpansionMode;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.measures.Size;
|
||||
|
||||
public interface ISelectionProcessor {
|
||||
public interface ISelectionProcessor extends IGroupProcessor {
|
||||
|
||||
/**
|
||||
* Returns the current {@link ComponentTransferable}.
|
||||
@@ -89,16 +89,6 @@ public interface ISelectionProcessor {
|
||||
*/
|
||||
void rotateSelection(int direction);
|
||||
|
||||
/**
|
||||
* Groups all selected components.
|
||||
*/
|
||||
void groupSelectedComponents();
|
||||
|
||||
/**
|
||||
* Ungroups all selected components.
|
||||
*/
|
||||
void ungroupSelectedComponents();
|
||||
|
||||
/**
|
||||
* Mirrors selected components in the given axis.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.common;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class Percentage implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private BigDecimal value;
|
||||
|
||||
public Percentage(BigDecimal value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Percentage(double value) {
|
||||
this.value = BigDecimal.valueOf(value);
|
||||
}
|
||||
|
||||
public Percentage(int value) {
|
||||
this.value = BigDecimal.valueOf((long) value);
|
||||
}
|
||||
|
||||
public BigDecimal getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(BigDecimal value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((value == null) ? 0 : value.stripTrailingZeros().hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Percentage other = (Percentage) obj;
|
||||
if (value == null) {
|
||||
if (other.value != null)
|
||||
return false;
|
||||
} else if (value.compareTo(other.value) != 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (value == null) return "null%";
|
||||
BigDecimal stripped = value.stripTrailingZeros();
|
||||
return String.format("%s%%", stripped.toPlainString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() throws CloneNotSupportedException {
|
||||
return super.clone();
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,11 @@ package org.diylc.common;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.core.IDynamicPropertySource;
|
||||
import org.diylc.core.IPropertyValidator;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
/**
|
||||
* Entity class for editable properties extracted from component objects. Represents a single
|
||||
@@ -65,7 +68,8 @@ public class PropertyWrapper implements Cloneable {
|
||||
public void readFrom(Object object) throws IllegalArgumentException, IllegalAccessException,
|
||||
InvocationTargetException, SecurityException, NoSuchMethodException {
|
||||
this.ownerObject = object;
|
||||
this.value = getGetter().invoke(object);
|
||||
Object value = getGetter().invoke(object);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
// public void readUniqueFrom(IDIYComponent component)
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -52,6 +53,7 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected UUID id = UUID.randomUUID();
|
||||
protected String name = "";
|
||||
|
||||
public static Color SELECTION_COLOR = Color.red;
|
||||
@@ -62,6 +64,7 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
|
||||
public static Color LIGHT_METAL_COLOR = Color.decode("#EEEEEE");
|
||||
public static Color COPPER_COLOR = Color.decode("#DA8A67");
|
||||
public static Color GOLD_COLOR = Color.decode("#F4B301");
|
||||
public static Color BRASS_COLOR = Color.decode("#B8860B");
|
||||
public static Color FR4_COLOR = Color.decode("#6B9B6B");
|
||||
public static Color FR4_LIGHT_COLOR = Color.decode("#BABE71");
|
||||
public static Color PHENOLIC_COLOR = Color.decode("#F8EBB3");
|
||||
@@ -78,6 +81,19 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getId() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canControlPointOverlap(int index) {
|
||||
return false;
|
||||
@@ -186,6 +202,10 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(this);
|
||||
|
||||
if (field.getName().equalsIgnoreCase("id")) {
|
||||
value = getId();
|
||||
}
|
||||
|
||||
// Deep copy point arrays.
|
||||
// TODO: something nicer
|
||||
if (value != null && value.getClass().isArray()
|
||||
@@ -275,14 +295,39 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSectionNames(int pointIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPointMoveFreely(int pointIndex) {
|
||||
public boolean canPointMoveFreely(int pointIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stretchBetween(Point2D first, Point2D second) {
|
||||
int count = getControlPointCount();
|
||||
// fresh instances because implementations are free to keep the point objects they are handed
|
||||
setControlPoint(new Point2D.Double(first.getX(), first.getY()), 0);
|
||||
if (count > 1) {
|
||||
setControlPoint(new Point2D.Double(second.getX(), second.getY()), count - 1);
|
||||
}
|
||||
for (int i = 1; i < count - 1; i++) {
|
||||
setControlPoint(getStretchedPoint(first, second, i), i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides where a control point that is neither of the two ends lands when the component is
|
||||
* stretched between them, see {@link #stretchBetween(Point2D, Point2D)}. The default spaces the
|
||||
* points evenly along the straight line joining the ends. Override to place a point that carries
|
||||
* a meaning of its own, such as one that positions a label.
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
* @param index
|
||||
* @return position of the control point at the specified index.
|
||||
*/
|
||||
protected Point2D getStretchedPoint(Point2D first, Point2D second, int index) {
|
||||
double t = (double) index / (getControlPointCount() - 1);
|
||||
return new Point2D.Double(first.getX() + t * (second.getX() - first.getX()),
|
||||
first.getY() + t * (second.getY() - first.getY()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle2D getCachingBounds() {
|
||||
|
||||
@@ -37,16 +37,14 @@ import com.thoughtworks.xstream.annotations.XStreamOmitField;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.LineStyle;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.*;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComponent<T> {
|
||||
public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComponent<T> implements
|
||||
IHaveLength {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -54,6 +52,7 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
|
||||
|
||||
public static Color GUIDELINE_COLOR = Color.blue;
|
||||
public static Size DEFAULT_SIZE = new Size(1d, SizeUnit.in);
|
||||
public static double CREATION_CURVATURE = 0.1;
|
||||
|
||||
// for backward compatibility
|
||||
protected Point2D[] controlPoints = null;
|
||||
@@ -151,10 +150,7 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
|
||||
path.curveTo(p[4].getX(), p[4].getY(), p[5].getX(), p[5].getY(), p[6].getX(), p[6].getY());
|
||||
}
|
||||
|
||||
Composite oldComposite = g2d.getComposite();
|
||||
if (alpha < MAX_ALPHA) {
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
|
||||
}
|
||||
Composite oldComposite = applyAlpha(g2d);
|
||||
|
||||
drawCurve(path, g2d, componentState, drawingObserver);
|
||||
|
||||
@@ -292,6 +288,72 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Waves the curve from side to side instead of leaving the intermediate points on the straight
|
||||
* line joining the ends, so that a freshly created component reads as a slack wire that can be
|
||||
* reshaped rather than as a rigid line. Successive handles go to opposite sides, which turns the
|
||||
* default four point curve into an S, while the points that lie on the curve itself stay on the
|
||||
* straight line and the sine envelope fades the excursions out towards both ends. The first
|
||||
* excursion always points down the screen so that the shape does not depend on which end was
|
||||
* drawn first.
|
||||
* <p>
|
||||
* Keeping the on-curve points on the line is what makes the wave survive: a handle either side
|
||||
* of one of them then comes out as its exact mirror, which is the continuity rule that the
|
||||
* smoothing in {@link #draw} enforces, so that pass leaves the shape alone instead of pulling
|
||||
* one handle across to satisfy it.
|
||||
*/
|
||||
@Override
|
||||
protected Point2D getStretchedPoint(Point2D first, Point2D second, int index) {
|
||||
Point2D p = super.getStretchedPoint(first, second, index);
|
||||
double dx = second.getX() - first.getX();
|
||||
double dy = second.getY() - first.getY();
|
||||
double length = Math.hypot(dx, dy);
|
||||
if (length == 0 || isOnCurve(index)) {
|
||||
return p;
|
||||
}
|
||||
if (dx < 0 || (dx == 0 && dy > 0)) {
|
||||
dx = -dx;
|
||||
dy = -dy;
|
||||
}
|
||||
double t = (double) index / (getControlPointCount() - 1);
|
||||
double offset = getStretchedSide(index) * CREATION_CURVATURE * length * Math.sin(Math.PI * t);
|
||||
return new Point2D.Double(p.getX() - dy / length * offset, p.getY() + dx / length * offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index
|
||||
* @return true if the control point at the specified index lies on the curve rather than pulling
|
||||
* it from the side, which follows from how {@link #draw} chains the segments together.
|
||||
*/
|
||||
private boolean isOnCurve(int index) {
|
||||
if (index == 0 || index == getControlPointCount() - 1) {
|
||||
return true;
|
||||
}
|
||||
if (getPointCount() == PointCount.FIVE) {
|
||||
return index == 2;
|
||||
}
|
||||
if (getPointCount() == PointCount.SEVEN) {
|
||||
return index == 3;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index
|
||||
* @return which side of the straight line the handle at the specified index is pushed to,
|
||||
* counting handles rather than control points so that an on-curve point in between does
|
||||
* not break the alternation.
|
||||
*/
|
||||
private int getStretchedSide(int index) {
|
||||
int handleCount = 0;
|
||||
for (int i = 1; i < index; i++) {
|
||||
if (!isOnCurve(i)) {
|
||||
handleCount++;
|
||||
}
|
||||
}
|
||||
return handleCount % 2 == 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
private Point2D findThirdPoint(Point2D p0, Point2D p) {
|
||||
return new Point2D.Double(2 * p0.getX() - p.getX(), 2 * p0.getY() - p.getY());
|
||||
}
|
||||
@@ -337,8 +399,12 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
|
||||
// }
|
||||
}
|
||||
|
||||
@EditableProperty
|
||||
@EditableProperty(name = "Length")
|
||||
public Size getLength() {
|
||||
return calculateLength();
|
||||
}
|
||||
|
||||
public Size calculateLength() {
|
||||
double d = 0;
|
||||
try {
|
||||
Point2D[] p = getControlPoints();
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Composite;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.LineStyle;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.common.OrientationHV;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.IHaveLength;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
/**
|
||||
* Base class for connectors that run only horizontally and vertically and turn at rounded
|
||||
* 90-degree corners.
|
||||
* <p>
|
||||
* The first and the last control point are the sticky ends. Each control point in between is a free
|
||||
* bend vertex that the route reaches with a horizontal move followed by a vertical one, or the
|
||||
* other way around when {@link #getStartDirection()} is vertical. Both coordinates of a bend point
|
||||
* therefore carry meaning and the arrangement never has to be guessed: an orthogonal route of
|
||||
* <code>s</code> segments has <code>s - 2</code> degrees of freedom, and this template spends
|
||||
* exactly two per bend point. Segments of zero length collapse silently, which is how the simple L,
|
||||
* horizontal-vertical-horizontal and vertical-horizontal-vertical shapes are expressed: a bend
|
||||
* point sharing a column with the start swallows the leading segment, one sharing a row with the
|
||||
* end swallows the trailing one. All four combinations of leaving and arriving on either axis are
|
||||
* therefore reachable by dragging alone, which is why the start direction is not offered as a
|
||||
* property.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public abstract class AbstractOrthogonalComponent<T> extends AbstractTransparentComponent<T>
|
||||
implements IHaveLength {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static Size DEFAULT_SIZE = new Size(1d, SizeUnit.in);
|
||||
public static Size DEFAULT_CORNER_RADIUS = new Size(0.05d, SizeUnit.in);
|
||||
|
||||
// rounding is skipped below this radius in pixels, where a quadratic corner is indistinguishable
|
||||
// from a sharp one
|
||||
private static final double MIN_CORNER_RADIUS = 1d;
|
||||
|
||||
private static final double EPSILON = 1e-6;
|
||||
|
||||
protected Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0),
|
||||
new Point2D.Double(DEFAULT_SIZE.convertToPixels() / 2, DEFAULT_SIZE.convertToPixels()),
|
||||
new Point2D.Double(DEFAULT_SIZE.convertToPixels(), DEFAULT_SIZE.convertToPixels())};
|
||||
|
||||
protected Color color = getDefaultColor();
|
||||
protected PointCount pointCount = PointCount.THREE;
|
||||
protected OrientationHV startDirection = OrientationHV.HORIZONTAL;
|
||||
protected LineStyle style = LineStyle.SOLID;
|
||||
protected Size cornerRadius = DEFAULT_CORNER_RADIUS;
|
||||
|
||||
/**
|
||||
* Draws the specified route onto graphics.
|
||||
*
|
||||
* @param curve
|
||||
* @param g2d
|
||||
* @param componentState
|
||||
* @param drawingObserver
|
||||
*/
|
||||
protected abstract void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver);
|
||||
|
||||
/**
|
||||
* @return default color.
|
||||
*/
|
||||
protected abstract Color getDefaultColor();
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
Path2D path = createIconPath(width, height);
|
||||
g2d.setColor(getDefaultColor().darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3));
|
||||
g2d.draw(path);
|
||||
g2d.setColor(getDefaultColor());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param width
|
||||
* @param height
|
||||
* @return the elbow drawn in the toolbox, so that subclasses that stroke it differently do not
|
||||
* have to restate the shape.
|
||||
*/
|
||||
protected Path2D createIconPath(int width, int height) {
|
||||
Path2D path = new Path2D.Double();
|
||||
path.moveTo(1, height - 3);
|
||||
path.lineTo(width / 2d - 2, height - 3);
|
||||
path.quadTo(width / 2d, height - 3, width / 2d, height - 5);
|
||||
path.lineTo(width / 2d, 5);
|
||||
path.quadTo(width / 2d, 3, width / 2d + 2, 3);
|
||||
path.lineTo(width - 1, 3);
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
if (checkPointsClipped(g2d.getClip())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Composite oldComposite = applyAlpha(g2d);
|
||||
|
||||
drawCurve(buildPath(), g2d, componentState, drawingObserver);
|
||||
|
||||
g2d.setComposite(oldComposite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the control points into the vertices of the route. Every control point is reached by a
|
||||
* move along one axis followed by a move along the other, the end point included, so the loop
|
||||
* below is the whole routing rule. Zero-length segments are dropped as they are produced.
|
||||
*
|
||||
* @return route vertices, starting with the first and ending with the last control point.
|
||||
*/
|
||||
protected List<Point2D> buildVertices() {
|
||||
Point2D[] p = getControlPoints();
|
||||
boolean horizontalFirst = getStartDirection() == OrientationHV.HORIZONTAL;
|
||||
List<Point2D> vertices = new ArrayList<Point2D>();
|
||||
addVertex(vertices, p[0]);
|
||||
for (int i = 1; i < p.length; i++) {
|
||||
Point2D previous = p[i - 1];
|
||||
Point2D next = p[i];
|
||||
if (horizontalFirst) {
|
||||
addVertex(vertices, new Point2D.Double(next.getX(), previous.getY()));
|
||||
} else {
|
||||
addVertex(vertices, new Point2D.Double(previous.getX(), next.getY()));
|
||||
}
|
||||
addVertex(vertices, next);
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the route as a path with rounded corners. The radius is clamped to half of each adjacent
|
||||
* segment so that short segments bend rather than overshoot.
|
||||
*
|
||||
* @return path following the route.
|
||||
*/
|
||||
protected Path2D buildPath() {
|
||||
List<Point2D> vertices = buildVertices();
|
||||
double radius = getCornerRadius().convertToPixels();
|
||||
Path2D path = new Path2D.Double();
|
||||
Point2D first = vertices.get(0);
|
||||
path.moveTo(first.getX(), first.getY());
|
||||
for (int i = 1; i < vertices.size() - 1; i++) {
|
||||
Point2D previous = vertices.get(i - 1);
|
||||
Point2D corner = vertices.get(i);
|
||||
Point2D next = vertices.get(i + 1);
|
||||
double r = Math.min(radius, Math.min(previous.distance(corner), corner.distance(next)) / 2);
|
||||
if (r < MIN_CORNER_RADIUS) {
|
||||
path.lineTo(corner.getX(), corner.getY());
|
||||
continue;
|
||||
}
|
||||
Point2D in = interpolate(corner, previous, r);
|
||||
Point2D out = interpolate(corner, next, r);
|
||||
path.lineTo(in.getX(), in.getY());
|
||||
path.quadTo(corner.getX(), corner.getY(), out.getX(), out.getY());
|
||||
}
|
||||
Point2D last = vertices.get(vertices.size() - 1);
|
||||
path.lineTo(last.getX(), last.getY());
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void addVertex(List<Point2D> vertices, Point2D p) {
|
||||
if (!vertices.isEmpty() && vertices.get(vertices.size() - 1).distance(p) < EPSILON) {
|
||||
return;
|
||||
}
|
||||
vertices.add(p);
|
||||
}
|
||||
|
||||
private static Point2D interpolate(Point2D from, Point2D towards, double distance) {
|
||||
double t = distance / from.distance(towards);
|
||||
return new Point2D.Double(from.getX() + (towards.getX() - from.getX()) * t, from.getY()
|
||||
+ (towards.getY() - from.getY()) * t);
|
||||
}
|
||||
|
||||
protected Point2D[] getControlPoints() {
|
||||
return this.controlPoints;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Point Count")
|
||||
public PointCount getPointCount() {
|
||||
if (pointCount == null) {
|
||||
pointCount = PointCount.THREE;
|
||||
}
|
||||
return pointCount;
|
||||
}
|
||||
|
||||
public void setPointCount(PointCount pointCount) {
|
||||
if (getPointCount() == pointCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
Point2D[] p = getControlPoints();
|
||||
Point2D start = p[0];
|
||||
Point2D end = p[p.length - 1];
|
||||
Point2D[] newPoints = new Point2D[pointCount.count];
|
||||
newPoints[0] = start;
|
||||
newPoints[pointCount.count - 1] = end;
|
||||
|
||||
int bendCount = pointCount.count - 2;
|
||||
for (int i = 1; i <= bendCount; i++) {
|
||||
newPoints[i] = getBendPoint(start, end, i, bendCount);
|
||||
}
|
||||
|
||||
this.controlPoints = newPoints;
|
||||
this.pointCount = pointCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Point2D getStretchedPoint(Point2D first, Point2D second, int index) {
|
||||
return getBendPoint(first, second, index, getControlPointCount() - 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spreads the bend points so that the route comes out as an even staircase: they advance evenly
|
||||
* along the axis the route travels last, and sit halfway between those steps on the other axis.
|
||||
*
|
||||
* @param start
|
||||
* @param end
|
||||
* @param index
|
||||
* @param bendCount number of control points between the two ends
|
||||
* @return position of the bend point at the specified index.
|
||||
*/
|
||||
private Point2D getBendPoint(Point2D start, Point2D end, int index, int bendCount) {
|
||||
double along = (double) index / bendCount;
|
||||
double across = (2d * index - 1) / (2d * bendCount);
|
||||
boolean horizontalFirst = getStartDirection() == OrientationHV.HORIZONTAL;
|
||||
double tx = horizontalFirst ? across : along;
|
||||
double ty = horizontalFirst ? along : across;
|
||||
return new Point2D.Double(start.getX() + (end.getX() - start.getX()) * tx, start.getY()
|
||||
+ (end.getY() - start.getY()) * ty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Not an editable property on purpose. Every route shape is reachable by dragging the bend
|
||||
* points, so exposing this would only give the user a second, less discoverable way to do the
|
||||
* same thing. It is kept as state because a quarter-turn rotation swaps the two axes and the
|
||||
* transformer has to record that.
|
||||
*
|
||||
* @return axis the route travels along when it leaves the first control point.
|
||||
*/
|
||||
public OrientationHV getStartDirection() {
|
||||
if (startDirection == null) {
|
||||
startDirection = OrientationHV.HORIZONTAL;
|
||||
}
|
||||
return startDirection;
|
||||
}
|
||||
|
||||
public void setStartDirection(OrientationHV startDirection) {
|
||||
this.startDirection = startDirection;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Corner Radius")
|
||||
public Size getCornerRadius() {
|
||||
if (cornerRadius == null) {
|
||||
cornerRadius = DEFAULT_CORNER_RADIUS;
|
||||
}
|
||||
return cornerRadius;
|
||||
}
|
||||
|
||||
public void setCornerRadius(Size cornerRadius) {
|
||||
this.cornerRadius = cornerRadius;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Color")
|
||||
public Color getLeadColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setLeadColor(Color color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Style")
|
||||
public LineStyle getStyle() {
|
||||
if (style == null) {
|
||||
style = LineStyle.SOLID;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
public void setStyle(LineStyle style) {
|
||||
this.style = style;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Length")
|
||||
public Size getLength() {
|
||||
return calculateLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Size calculateLength() {
|
||||
// corner rounding shaves a fraction of the radius off each bend, which is well below the
|
||||
// precision this readout is used at
|
||||
List<Point2D> vertices = buildVertices();
|
||||
double d = 0;
|
||||
for (int i = 1; i < vertices.size(); i++) {
|
||||
d += vertices.get(i - 1).distance(vertices.get(i));
|
||||
}
|
||||
|
||||
SizeUnit unit;
|
||||
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.METRIC_KEY, true)) {
|
||||
unit = SizeUnit.mm;
|
||||
} else {
|
||||
unit = SizeUnit.in;
|
||||
}
|
||||
|
||||
return new Size(d * SizeUnit.px.getFactor() / unit.getFactor(), unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return getControlPoints().length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return getControlPoints()[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
getControlPoints()[index].setLocation(point);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return index == 0 || index == getControlPointCount() - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canControlPointOverlap(int index) {
|
||||
return index > 0 && index < getControlPointCount() - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.WHEN_SELECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle2D getCachingBounds() {
|
||||
double minX = Integer.MAX_VALUE;
|
||||
double maxX = Integer.MIN_VALUE;
|
||||
double minY = Integer.MAX_VALUE;
|
||||
double maxY = Integer.MIN_VALUE;
|
||||
double margin = 50; // hard coded, replace with something meaningful
|
||||
// every route vertex borrows both of its coordinates from a control point, so the control
|
||||
// points alone bound the whole route
|
||||
for (int i = 0; i < getControlPointCount(); i++) {
|
||||
Point2D p = getControlPoint(i);
|
||||
if (p.getX() < minX) {
|
||||
minX = p.getX();
|
||||
}
|
||||
if (p.getX() > maxX) {
|
||||
maxX = p.getX();
|
||||
}
|
||||
if (p.getY() < minY) {
|
||||
minY = p.getY();
|
||||
}
|
||||
if (p.getY() > maxY) {
|
||||
maxY = p.getY();
|
||||
}
|
||||
}
|
||||
|
||||
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin, maxY - minY
|
||||
+ 2 * margin);
|
||||
}
|
||||
|
||||
public enum PointCount {
|
||||
THREE(3), FOUR(4), FIVE(5);
|
||||
|
||||
private int count;
|
||||
|
||||
private PointCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name().substring(0, 1) + name().substring(1).toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-9
@@ -22,24 +22,57 @@
|
||||
package org.diylc.components;
|
||||
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.common.Percentage;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public abstract class AbstractTransparentComponent<T> extends AbstractComponent<T> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final boolean OUTLINE_MODE_WHILE_DRAGGING = false;
|
||||
|
||||
public static byte MAX_ALPHA = 127;
|
||||
public static byte MAX_ALPHA_LEGACY = Byte.MAX_VALUE;
|
||||
public static int MAX_ALPHA = 100;
|
||||
|
||||
protected Byte alpha = MAX_ALPHA;
|
||||
@Deprecated
|
||||
protected Byte alpha;
|
||||
|
||||
@EditableProperty
|
||||
public Byte getAlpha() {
|
||||
if (alpha == null) {
|
||||
alpha = MAX_ALPHA;
|
||||
protected Percentage alphaPercent = new Percentage(100);
|
||||
|
||||
@EditableProperty(name = "Alpha")
|
||||
public Percentage getAlpha() {
|
||||
if (alphaPercent == null) {
|
||||
if (alpha != null) {
|
||||
alphaPercent = new Percentage(Math.round(100f * alpha / MAX_ALPHA_LEGACY));
|
||||
alpha = null;
|
||||
} else {
|
||||
alphaPercent = new Percentage(100);
|
||||
}
|
||||
}
|
||||
return alpha;
|
||||
return alphaPercent;
|
||||
}
|
||||
|
||||
public void setAlpha(Byte alpha) {
|
||||
this.alpha = alpha;
|
||||
public void setAlpha(Percentage alphaPercent) {
|
||||
this.alphaPercent = alphaPercent;
|
||||
}
|
||||
|
||||
public Composite applyAlpha(Graphics2D g2d, ComponentState componentState) {
|
||||
Composite oldComposite = g2d.getComposite();
|
||||
double alpha = (OUTLINE_MODE_WHILE_DRAGGING && componentState == ComponentState.DRAGGING) ? 0 :
|
||||
getAlpha().getValue().doubleValue();
|
||||
if (alpha < MAX_ALPHA) {
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)(alpha / 100d)));
|
||||
}
|
||||
return oldComposite;
|
||||
}
|
||||
|
||||
public Composite applyAlpha(Graphics2D g2d) {
|
||||
Composite oldComposite = g2d.getComposite();
|
||||
double alpha = getAlpha().getValue().doubleValue();
|
||||
if (alpha < MAX_ALPHA) {
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)(alpha / 100d)));
|
||||
}
|
||||
return oldComposite;
|
||||
}
|
||||
}
|
||||
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components.composite;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Composite;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.diylc.components.AbstractComponent;
|
||||
import org.diylc.components.AbstractTransparentComponent;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.annotations.BomPolicy;
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
/**
|
||||
* Instantiation of a {@code BuildingBlock} as a single, rigid component rather than a group of
|
||||
* loose ones. Wraps an embedded list of child components and aggregates their control points,
|
||||
* so it can be placed, moved, rotated and wired up like any other component while the netlist,
|
||||
* BOM and AI project description still see its individual terminals (see
|
||||
* {@link #getControlPointNodeName(int)}).
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
@ComponentDescriptor(
|
||||
name = "Building Block",
|
||||
description = "A group of components instantiated as a single rigid component",
|
||||
category = "Building Blocks",
|
||||
author = "Branislav Stojkovic",
|
||||
instanceNamePrefix = "BLK",
|
||||
zOrder = IDIYComponent.COMPONENT,
|
||||
flexibleZOrder = true,
|
||||
bomPolicy = BomPolicy.SHOW_ALL_NAMES,
|
||||
autoEdit = false,
|
||||
enableCache = false,
|
||||
transformer = CompositeComponentTransformer.class,
|
||||
hiddenInPalette = true)
|
||||
public class CompositeComponent extends AbstractTransparentComponent<String> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private List<IDIYComponent<?>> childComponents = new ArrayList<IDIYComponent<?>>();
|
||||
|
||||
// Name of the building block this instance was created from. Drives its display identity in
|
||||
// the BOM, netlist report and AI project description (see ComponentProcessor.getDisplayTypeName).
|
||||
private String blockName;
|
||||
|
||||
// Free-form label the user can edit after placement, independent of blockName. Shown in the BOM.
|
||||
private String value;
|
||||
|
||||
// Flat index mapping a composite control point index to (child ordinal, child point index).
|
||||
// Rebuilt lazily since children never change after construction and the arrays are not
|
||||
// serialized (transient).
|
||||
private transient int[] childOf;
|
||||
private transient int[] pointOf;
|
||||
private transient String[] nodeNameOf;
|
||||
|
||||
/**
|
||||
* Required no-arg constructor, used reflectively by {@code ComponentProcessor} and XStream.
|
||||
*/
|
||||
public CompositeComponent() {
|
||||
super();
|
||||
}
|
||||
|
||||
private void buildIndexIfNeeded() {
|
||||
if (childOf != null) {
|
||||
return;
|
||||
}
|
||||
int total = 0;
|
||||
for (IDIYComponent<?> c : childComponents) {
|
||||
total += c.getControlPointCount();
|
||||
}
|
||||
int[] newChildOf = new int[total];
|
||||
int[] newPointOf = new int[total];
|
||||
int idx = 0;
|
||||
for (int ci = 0; ci < childComponents.size(); ci++) {
|
||||
IDIYComponent<?> c = childComponents.get(ci);
|
||||
int count = c.getControlPointCount();
|
||||
for (int pi = 0; pi < count; pi++) {
|
||||
newChildOf[idx] = ci;
|
||||
newPointOf[idx] = pi;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
nodeNameOf = buildNodeNames(newChildOf, newPointOf);
|
||||
pointOf = newPointOf;
|
||||
// assigned last, it is what marks the index as built
|
||||
childOf = newChildOf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the node names the composite exposes. A child's own node name is kept unchanged
|
||||
* whenever it is unique, so the pads of a custom board read "VCC" rather than "Pad3.VCC" - the
|
||||
* pad's component name is an arbitrary leftover from the project the block was saved in.
|
||||
* Clashing names are resolved in two passes: pins of multi-point children are qualified with the
|
||||
* child's name ("R1.1", "R2.1"), then whatever still clashes, typically several pads sharing a
|
||||
* name, gets a numeric suffix in save order ("GND_1", "GND_2").
|
||||
*
|
||||
* <p>Only sticky, named points take part. Non-sticky points never become netlist nodes, and a
|
||||
* board's corner points would otherwise force a rename on a pad that shares their default
|
||||
* numeric name.
|
||||
*/
|
||||
private String[] buildNodeNames(int[] newChildOf, int[] newPointOf) {
|
||||
String[] names = new String[newChildOf.length];
|
||||
boolean[] terminal = new boolean[newChildOf.length];
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
IDIYComponent<?> child = childComponents.get(newChildOf[i]);
|
||||
names[i] = child.getControlPointNodeName(newPointOf[i]);
|
||||
terminal[i] = names[i] != null && child.isControlPointSticky(newPointOf[i]);
|
||||
}
|
||||
|
||||
Set<String> clashing = findClashingNames(names, terminal);
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
IDIYComponent<?> child = childComponents.get(newChildOf[i]);
|
||||
String childName = child.getName();
|
||||
if (terminal[i] && clashing.contains(names[i]) && child.getControlPointCount() > 1
|
||||
&& childName != null && !childName.trim().isEmpty()) {
|
||||
names[i] = childName.trim() + "." + names[i];
|
||||
}
|
||||
}
|
||||
|
||||
clashing = findClashingNames(names, terminal);
|
||||
if (clashing.isEmpty()) {
|
||||
return names;
|
||||
}
|
||||
Set<String> taken = new HashSet<String>();
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
if (terminal[i] && !clashing.contains(names[i])) {
|
||||
taken.add(names[i]);
|
||||
}
|
||||
}
|
||||
Map<String, Integer> nextSuffix = new HashMap<String, Integer>();
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
if (terminal[i] && clashing.contains(names[i])) {
|
||||
int suffix = nextSuffix.getOrDefault(names[i], 1);
|
||||
while (taken.contains(names[i] + "_" + suffix)) {
|
||||
suffix++;
|
||||
}
|
||||
nextSuffix.put(names[i], suffix + 1);
|
||||
names[i] = names[i] + "_" + suffix;
|
||||
taken.add(names[i]);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private static Set<String> findClashingNames(String[] names, boolean[] terminal) {
|
||||
Set<String> seen = new HashSet<String>();
|
||||
Set<String> clashing = new HashSet<String>();
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
if (terminal[i] && !seen.add(names[i])) {
|
||||
clashing.add(names[i]);
|
||||
}
|
||||
}
|
||||
return clashing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the embedded child components, in save order. Never {@code null}.
|
||||
*/
|
||||
public List<IDIYComponent<?>> getChildComponents() {
|
||||
if (childComponents == null) {
|
||||
childComponents = new ArrayList<IDIYComponent<?>>();
|
||||
}
|
||||
return childComponents;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Block", defaultable = false, sortOrder = 1)
|
||||
public String getBlockName() {
|
||||
return blockName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source block name. Deliberately not named {@code setBlockName} so that
|
||||
* {@code ComponentProcessor}'s getter/setter matching does not turn the "Block" property
|
||||
* editable in the property editor - it is metadata about the instance, not a value the user
|
||||
* should be able to change after placement.
|
||||
*/
|
||||
public void initBlockName(String blockName) {
|
||||
this.blockName = blockName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
buildIndexIfNeeded();
|
||||
return childOf.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).getControlPoint(pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
buildIndexIfNeeded();
|
||||
childComponents.get(childOf[index]).setControlPoint(point, pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).isControlPointSticky(pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canControlPointOverlap(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).canControlPointOverlap(pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return childComponents.get(childOf[index]).getControlPointVisibilityPolicy(pointOf[index]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
buildIndexIfNeeded();
|
||||
return nodeNameOf[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* The composite is always rigid: it moves and rotates as a unit and never stretches. See design
|
||||
* decision D4 in {@code docs/plans/composite-building-blocks.md}.
|
||||
*/
|
||||
@Override
|
||||
public boolean canPointMoveFreely(int pointIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createdIn(Project project) {
|
||||
for (IDIYComponent<?> c : getChildComponents()) {
|
||||
c.createdIn(project);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@EditableProperty
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
Composite oldComposite = applyAlpha(g2d, componentState);
|
||||
for (IDIYComponent<?> c : getChildComponents()) {
|
||||
// Children always draw in their normal state - the composite draws its own selection
|
||||
// outline below instead of letting every child paint its own highlight.
|
||||
c.draw(g2d, componentState, outlineMode, project, drawingObserver);
|
||||
}
|
||||
g2d.setComposite(oldComposite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
// Must not touch `components` - ComponentProcessor draws this on a default-constructed,
|
||||
// childless instance to build the palette icon.
|
||||
g2d.setColor(LIGHT_METAL_COLOR);
|
||||
g2d.fillRect(2, 2, width - 4, height - 4);
|
||||
g2d.setColor(METAL_COLOR);
|
||||
g2d.drawRect(2, 2, width - 4, height - 4);
|
||||
int halfW = width / 2;
|
||||
int halfH = height / 2;
|
||||
g2d.drawLine(halfW, 2, halfW, height - 2);
|
||||
g2d.drawLine(2, halfH, width - 2, halfH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep-clones the composite together with all its children, so that two placed instances never
|
||||
* share child objects (that would make moving one instance move the other - see the "clone()
|
||||
* is the sharp edge" risk in {@code docs/plans/composite-building-blocks.md}). Preserves this
|
||||
* component's own id and every child's id, matching {@link AbstractComponent#clone()}'s
|
||||
* contract of copying identity by default - callers that need a genuinely new component
|
||||
* reassign ids themselves, exactly as {@code BuildingBlockManager.loadBlock} does when it
|
||||
* clones each source component individually to place a brand new instance of a block. This also
|
||||
* keeps id-sensitive reflective comparisons (like the default {@code equalsTo} most child
|
||||
* component classes inherit) working correctly between a composite and its own clone, which the
|
||||
* undo dirty-check in {@code Presenter.notifyProjectModifiedIfNeeded} relies on.
|
||||
*/
|
||||
@Override
|
||||
public IDIYComponent<String> clone() throws CloneNotSupportedException {
|
||||
CompositeComponent clone = new CompositeComponent();
|
||||
clone.setId(getId());
|
||||
clone.setName(getName());
|
||||
clone.blockName = this.blockName;
|
||||
clone.value = this.value;
|
||||
// matches AbstractComponent.clone(), which shallow-copies the Percentage reference
|
||||
clone.setAlpha(getAlpha());
|
||||
List<IDIYComponent<?>> clonedComponents = new ArrayList<IDIYComponent<?>>(getChildComponents().size());
|
||||
for (IDIYComponent<?> c : getChildComponents()) {
|
||||
clonedComponents.add(c.clone());
|
||||
}
|
||||
clone.childComponents = clonedComponents;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden because {@link AbstractComponent#equalsTo(IDIYComponent)} compares the
|
||||
* {@code components} field with {@code List.equals()}, which falls back to child object
|
||||
* identity since no component class overrides {@code Object.equals()} - two structurally
|
||||
* identical composites built from different object graphs (e.g. an original and its clone)
|
||||
* would otherwise never compare equal, defeating the undo dirty-check in
|
||||
* {@code Presenter.notifyProjectModifiedIfNeeded}.
|
||||
*/
|
||||
@Override
|
||||
public boolean equalsTo(IDIYComponent<?> other) {
|
||||
if (!(other instanceof CompositeComponent)) {
|
||||
return false;
|
||||
}
|
||||
CompositeComponent o = (CompositeComponent) other;
|
||||
if (!Objects.equals(getName(), o.getName()) || !Objects.equals(blockName, o.blockName)
|
||||
|| !Objects.equals(value, o.value) || !Objects.equals(getAlpha(), o.getAlpha())) {
|
||||
return false;
|
||||
}
|
||||
List<IDIYComponent<?>> otherComponents = o.getChildComponents();
|
||||
if (getChildComponents().size() != otherComponents.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < childComponents.size(); i++) {
|
||||
if (!childComponents.get(i).equalsTo(otherComponents.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components.composite;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.IComponentTransformer;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.presenter.ComponentProcessor;
|
||||
|
||||
/**
|
||||
* Rotates and mirrors a {@link CompositeComponent} by delegating to each child's own
|
||||
* {@link IComponentTransformer}, around the same centre for every child so the assembly stays
|
||||
* geometrically consistent. If any child's type cannot rotate (or mirror), the whole composite
|
||||
* reports that it cannot either - silently leaving one child un-rotated would be worse (see
|
||||
* design decision D9 in {@code docs/plans/composite-building-blocks.md}).
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class CompositeComponentTransformer implements IComponentTransformer {
|
||||
|
||||
@Override
|
||||
public boolean canRotate(IDIYComponent<?> component) {
|
||||
CompositeComponent composite = (CompositeComponent) component;
|
||||
if (composite.getChildComponents().isEmpty())
|
||||
return false;
|
||||
for (IDIYComponent<?> child : composite.getChildComponents()) {
|
||||
if (!childTransformer(child).canRotate(child))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canMirror(IDIYComponent<?> component) {
|
||||
CompositeComponent composite = (CompositeComponent) component;
|
||||
if (composite.getChildComponents().isEmpty())
|
||||
return false;
|
||||
for (IDIYComponent<?> child : composite.getChildComponents()) {
|
||||
if (!childTransformer(child).canMirror(child))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mirroringChangesCircuit() {
|
||||
// This transformer instance is shared by every CompositeComponent (ComponentProcessor caches
|
||||
// one instance per class), so it has no access to a specific composite's children here.
|
||||
// Conservatively assume mirroring can change the circuit, so the user is always warned - a
|
||||
// spurious confirmation is far cheaper than silently flipping a board's pinout.
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
|
||||
CompositeComponent composite = (CompositeComponent) component;
|
||||
for (IDIYComponent<?> child : composite.getChildComponents()) {
|
||||
childTransformer(child).rotate(child, center, direction);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
|
||||
CompositeComponent composite = (CompositeComponent) component;
|
||||
for (IDIYComponent<?> child : composite.getChildComponents()) {
|
||||
childTransformer(child).mirror(child, center, direction);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private IComponentTransformer childTransformer(IDIYComponent<?> child) {
|
||||
ComponentType type =
|
||||
ComponentProcessor.getInstance().extractComponentTypeFrom((Class) child.getClass());
|
||||
return type.getTransformer();
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components.transform;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.common.OrientationHV;
|
||||
import org.diylc.components.AbstractOrthogonalComponent;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Rotates and mirrors orthogonal connectors. Quarter-turn rotation turns every horizontal run of
|
||||
* the route into a vertical one, so the start direction has to follow the points. Mirroring leaves
|
||||
* both axes in place and needs nothing beyond moving the points.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class OrthogonalComponentTransformer extends SimpleComponentTransformer {
|
||||
|
||||
@Override
|
||||
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
|
||||
super.rotate(component, center, direction);
|
||||
if (component instanceof AbstractOrthogonalComponent<?> orthogonal) {
|
||||
orthogonal.setStartDirection(orthogonal.getStartDirection() == OrientationHV.HORIZONTAL
|
||||
? OrientationHV.VERTICAL : OrientationHV.HORIZONTAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.diylc.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ComponentGroup implements Serializable {
|
||||
|
||||
private Set<UUID> componentIds;
|
||||
private String name;
|
||||
|
||||
public ComponentGroup(Set<UUID> componentIds) {
|
||||
this.componentIds = componentIds;
|
||||
}
|
||||
|
||||
public ComponentGroup(Set<UUID> componentIds, String name) {
|
||||
this.componentIds = componentIds;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static ComponentGroup from(Collection<IDIYComponent<?>> components, String name) {
|
||||
Set<UUID> componentIds = components.stream()
|
||||
.map(IDIYComponent::getId)
|
||||
.filter(id -> id != null)
|
||||
.collect(Collectors.toSet());
|
||||
return new ComponentGroup(componentIds, name);
|
||||
}
|
||||
|
||||
public Set<UUID> getComponentIds() {
|
||||
return componentIds;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
@@ -55,6 +56,10 @@ public interface IDIYComponent<T> extends Serializable, Cloneable {
|
||||
public static final int WIRING = 5;
|
||||
public static final int TEXT = 6;
|
||||
|
||||
UUID getId();
|
||||
|
||||
void setId(UUID id);
|
||||
|
||||
/**
|
||||
* @return component instance name.
|
||||
*/
|
||||
@@ -100,6 +105,17 @@ public interface IDIYComponent<T> extends Serializable, Cloneable {
|
||||
*/
|
||||
void setControlPoint(Point2D point, int index);
|
||||
|
||||
/**
|
||||
* Positions the component so that it spans the two points, which is how components created with
|
||||
* <code>CreationMethod.POINT_BY_POINT</code> are shaped while the user is placing them. The first
|
||||
* and the last control point go to the two ends; where the points in between land is up to the
|
||||
* component.
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
*/
|
||||
void stretchBetween(Point2D first, Point2D second);
|
||||
|
||||
/**
|
||||
* @param index
|
||||
* @return true, if the specified control point may stick to control points of other components.
|
||||
@@ -132,13 +148,6 @@ public interface IDIYComponent<T> extends Serializable, Cloneable {
|
||||
*/
|
||||
String getInternalLinkName(int index1, int index2);
|
||||
|
||||
/**
|
||||
* @param pointIndex
|
||||
* @return name of the internal section containing the specified point, or null if the component does not have internal sections.
|
||||
* One point can be shared between multiple sections, in which case, the result may contain more than one element.
|
||||
*/
|
||||
String[] getSectionNames(int pointIndex);
|
||||
|
||||
/**
|
||||
* @param pointIndex
|
||||
* @return true if the control point can move without affecting other points' position, false otherwise
|
||||
@@ -199,13 +208,4 @@ public interface IDIYComponent<T> extends Serializable, Cloneable {
|
||||
* @param project
|
||||
*/
|
||||
void createdIn(Project project);
|
||||
|
||||
/**
|
||||
* Returns whether this component is polarized (direction matters for electronic function).
|
||||
* Default implementation returns true, components that are not polarized should override this.
|
||||
* @return true if the component is polarized, false otherwise
|
||||
*/
|
||||
default boolean isPolarized() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.core;
|
||||
|
||||
import org.diylc.core.measures.Size;
|
||||
|
||||
public interface IHaveLength {
|
||||
|
||||
Size calculateLength();
|
||||
}
|
||||
@@ -23,13 +23,9 @@ package org.diylc.core;
|
||||
|
||||
import java.awt.Font;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.diylc.appframework.update.VersionNumber;
|
||||
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
@@ -67,6 +63,7 @@ public class Project implements Serializable, Cloneable {
|
||||
private Integer dotSpacing;
|
||||
private List<IDIYComponent<?>> components;
|
||||
private Set<Set<IDIYComponent<?>>> groups;
|
||||
private Set<ComponentGroup> groupsEx;
|
||||
private Set<Integer> lockedLayers;
|
||||
private Set<IDIYComponent<?>> lockedComponents;
|
||||
private Set<Integer> hiddenLayers;
|
||||
@@ -75,6 +72,7 @@ public class Project implements Serializable, Cloneable {
|
||||
public Project() {
|
||||
components = new ArrayList<IDIYComponent<?>>();
|
||||
groups = new HashSet<Set<IDIYComponent<?>>>();
|
||||
groupsEx = new HashSet<>();
|
||||
lockedLayers = new HashSet<Integer>();
|
||||
hiddenLayers = new HashSet<Integer>();
|
||||
lockedComponents = new HashSet<IDIYComponent<?>>();
|
||||
@@ -153,22 +151,35 @@ public class Project implements Serializable, Cloneable {
|
||||
|
||||
/**
|
||||
* List of components sorted by z-order ascending.
|
||||
*
|
||||
* s
|
||||
* @return
|
||||
*/
|
||||
public List<IDIYComponent<?>> getComponents() {
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of grouped components.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Set<Set<IDIYComponent<?>>> getGroups() {
|
||||
return groups;
|
||||
public Set<ComponentGroup> getGroupsEx() {
|
||||
if (groupsEx == null) {
|
||||
groupsEx = new HashSet<>();
|
||||
|
||||
if (groups != null && !groups.isEmpty()) {
|
||||
for (Set<IDIYComponent<?>> group : groups) {
|
||||
Set<IDIYComponent<?>> nonNullIds =
|
||||
group.stream().filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
if (!nonNullIds.isEmpty()) {
|
||||
ComponentGroup newGroup = ComponentGroup.from(group, null);
|
||||
groupsEx.add(newGroup);
|
||||
}
|
||||
}
|
||||
groups.clear();
|
||||
}
|
||||
}
|
||||
groupsEx.removeAll(groupsEx.stream().
|
||||
filter(x -> x.getComponentIds() == null ||
|
||||
x.getComponentIds().stream().noneMatch(Objects::nonNull)).collect(Collectors.toSet()));
|
||||
return groupsEx;
|
||||
}
|
||||
|
||||
|
||||
public Set<IDIYComponent<?>> getLockedComponents() {
|
||||
if (lockedComponents == null)
|
||||
lockedComponents = new HashSet<IDIYComponent<?>>();
|
||||
@@ -292,6 +303,11 @@ public class Project implements Serializable, Cloneable {
|
||||
return false;
|
||||
} else if (!groups.equals(other.groups))
|
||||
return false;
|
||||
if (groupsEx == null) {
|
||||
if (other.groupsEx != null)
|
||||
return false;
|
||||
} else if (!groupsEx.equals(other.groupsEx))
|
||||
return false;
|
||||
if (lockedLayers == null) {
|
||||
if (other.lockedLayers != null)
|
||||
return false;
|
||||
@@ -356,12 +372,15 @@ public class Project implements Serializable, Cloneable {
|
||||
}
|
||||
}
|
||||
|
||||
for (Set<IDIYComponent<?>> group : this.groups) {
|
||||
Set<IDIYComponent<?>> cloneGroup = new HashSet<IDIYComponent<?>>();
|
||||
for (IDIYComponent<?> component : group) {
|
||||
cloneGroup.add(cloneMap.get(component));
|
||||
for (ComponentGroup group : this.getGroupsEx()) {
|
||||
// Filter out null IDs when cloning groups
|
||||
Set<UUID> validIds = group.getComponentIds().stream()
|
||||
.filter(id -> id != null)
|
||||
.collect(Collectors.toSet());
|
||||
// Only add group if it has valid component IDs
|
||||
if (!validIds.isEmpty()) {
|
||||
project.getGroupsEx().add(new ComponentGroup(validIds, group.getName()));
|
||||
}
|
||||
project.groups.add(cloneGroup);
|
||||
}
|
||||
|
||||
for (IDIYComponent<?> component : this.getLockedComponents()) {
|
||||
|
||||
@@ -123,9 +123,18 @@ public @interface ComponentDescriptor {
|
||||
boolean enableDatasheet() default false;
|
||||
|
||||
/**
|
||||
* If datasheet is enabled, this property determines how many columns define a component, e.g. in how many steps we can instantiate a component
|
||||
*
|
||||
* If datasheet is enabled, this property determines how many columns define a component, e.g. in how many steps we can instantiate a component
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
int datasheetCreationStepCount() default 0;
|
||||
|
||||
/**
|
||||
* @return true if the component type should not be listed in the palette (toolbox and
|
||||
* component tree), while still being resolvable via
|
||||
* {@code ComponentProcessor.extractComponentTypeFrom} for netlist, BOM and name
|
||||
* generation purposes. Used by component types that are only ever created
|
||||
* programmatically, e.g. {@code CompositeComponent}.
|
||||
*/
|
||||
boolean hiddenInPalette() default false;
|
||||
}
|
||||
|
||||
@@ -27,5 +27,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PercentEditor {
|
||||
|
||||
public byte _100PercentValue();
|
||||
public int minValue() default 0;
|
||||
|
||||
public int maxValue() default 100;
|
||||
}
|
||||
|
||||
@@ -1,80 +1,84 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.core.images;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
/**
|
||||
* Loads image resources as Icons.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public enum IconLoader {
|
||||
|
||||
Delete("delete.png"), Add("add.png"), FolderOut("folder_out.png"), Garbage("garbage.png"), DiskBlue("disk_blue.png"), SaveAs(
|
||||
"save_as.png"), Exit("exit.png"), DocumentPlainYellow("document_plain_yellow.png"), PhotoScenery(
|
||||
"photo_scenery.png"), LightBulbOn("lightbulb_on.png"), LightBulbOff("lightbulb_off.png"), NotebookAdd(
|
||||
"notebook_add.png"), FormGreen("form_green.png"), Gears("gears.png"), About("about.png"), WindowColors(
|
||||
"window_colors.png"), WindowGear("window_gear.png"), NavigateCheck("navigate_check.png"), Undo("undo.png"), Error(
|
||||
"error.png"), Warning("warning.png"), ZoomSmall("zoom_small.png"), MoveSmall("move_small.png"), Print("print.png"), PDF(
|
||||
"pdf.png"), Excel("excel.png"), CSV("csv.png"), HTML("html.png"), Image("image.png"), Cut("cut.png"), Copy(
|
||||
"copy.png"), Paste("paste.png"), Selection("selection.png"), BOM("bom.png"), BlackBoard("blackboard.png"), IdCard(
|
||||
"id_card.png"), IdCardAdd("id_card_add.png"), Chest("chest.png"), Upload("upload.png"), Wrench("wrench.png"), Group(
|
||||
"group.png"), Ungroup("ungroup.png"), TraceMask("trace_mask.png"), Faq("faq.png"), Component("component.png"), Plugin(
|
||||
"plugin.png"), Manual("manual.png"), Donate("donate.png"), Bug("bug.png"), IconLarge("icon_large.png"), IconMedium(
|
||||
"icon_medium.png"), IconSmall("icon_small.png"), DocumentEdit("document_edit.png"), EditComponent(
|
||||
"edit_component.png"), Size("size.png"), Front("front.png"), Back("back.png"), Pens("pens.png"), Sort("sort.png"), ElementsSelection(
|
||||
"elements_selection.png"), Schaller("schaller.png");
|
||||
|
||||
protected String name;
|
||||
|
||||
private IconLoader(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Icon getIcon() {
|
||||
java.net.URL imgURL = getClass().getResource("/diylc-core-images/" + name);
|
||||
if (imgURL != null) {
|
||||
return new ImageIcon(imgURL, name);
|
||||
} else {
|
||||
Logger.getLogger(IconLoader.class).error("Couldn't find file: " + name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
BufferedImage img = null;
|
||||
try {
|
||||
img = ImageIO.read(getClass().getResourceAsStream("/diylc-core-images/" + name));
|
||||
} catch (IOException e) {
|
||||
Logger.getLogger(IconLoader.class).error("Couldn't find file: " + name);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.core.images;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Loads image resources as Icons.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public enum IconLoader {
|
||||
|
||||
About("about.png"), Add("add.png"), BOM("bom.png"), Back("back.png"), BlackBoard(
|
||||
"blackboard.png"), Bug("bug.png"), CSV("csv.png"), Chest("chest.png"), Component(
|
||||
"component.png"), Copy("copy.png"), Cut("cut.png"), Delete("delete.png"), DiskBlue(
|
||||
"disk_blue.png"), DocumentEdit("document_edit.png"), DocumentPlainYellow(
|
||||
"document_plain_yellow.png"), Donate("donate.png"), EditComponent(
|
||||
"edit_component.png"), ElementsSelection("elements_selection.png"), Error("error.png"), Excel(
|
||||
"excel.png"), Exit("exit.png"), Faq("faq.png"), FolderOut("folder_out.png"), FormGreen(
|
||||
"form_green.png"), Front("front.png"), Garbage("garbage.png"), Gears("gears.png"), Group(
|
||||
"group.png"), HTML("html.png"), IconLarge("icon_large.png"), IconMedium(
|
||||
"icon_medium.png"), IconSmall("icon_small.png"), IdCard("id_card.png"), IdCardAdd(
|
||||
"id_card_add.png"), Image("image.png"), LightBulbOff("lightbulb_off.png"), LightBulbOn(
|
||||
"lightbulb_on.png"), Manual("manual.png"), MoveSmall("move_small.png"), NavigateCheck(
|
||||
"navigate_check.png"), NotebookAdd("notebook_add.png"), PDF("pdf.png"), Paste(
|
||||
"paste.png"), Pens("pens.png"), PhotoScenery("photo_scenery.png"), Plugin(
|
||||
"plugin.png"), Print("print.png"), SaveAs("save_as.png"), Schaller("schaller.png"), Selection(
|
||||
"selection.png"), Size("size.png"), Sort("sort.png"), TraceMask("trace_mask.png"), Undo(
|
||||
"undo.png"), Ungroup("ungroup.png"), Upload("upload.png"), Warning(
|
||||
"warning.png"), WindowColors("window_colors.png"), WindowGear("window_gear.png"), Wrench(
|
||||
"wrench.png"), ZoomSmall("zoom_small.png");
|
||||
|
||||
protected String name;
|
||||
|
||||
private IconLoader(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Icon getIcon() {
|
||||
java.net.URL imgURL = getClass().getResource("/diylc-core-images/" + name);
|
||||
if (imgURL != null) {
|
||||
return new ImageIcon(imgURL, name);
|
||||
} else {
|
||||
Logger.getLogger(IconLoader.class).error("Couldn't find file: " + name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
BufferedImage img = null;
|
||||
try {
|
||||
img = ImageIO.read(getClass().getResourceAsStream("/diylc-core-images/" + name));
|
||||
} catch (IOException e) {
|
||||
Logger.getLogger(IconLoader.class).error("Couldn't find file: " + name);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,4 +118,10 @@ public class Resistance extends AbstractMeasure<ResistanceUnit> {
|
||||
}
|
||||
throw new IllegalArgumentException("Could not parse resistance: " + value);
|
||||
}
|
||||
|
||||
public Double getNormalizedValue() {
|
||||
if (value == null || unit == null)
|
||||
return null;
|
||||
return value * unit.getFactor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
*
|
||||
* DIY Layout Creator (DIYLC).
|
||||
* Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
*
|
||||
* This file is part of DIYLC.
|
||||
*
|
||||
* DIYLC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* DIYLC is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.diylc.core.measures;
|
||||
|
||||
/**
|
||||
* Record containing resize dimensions for a component being resized.
|
||||
*
|
||||
* @param width the width of the component (in mm or in, depending on configuration)
|
||||
* @param height the height of the component (in mm or in, depending on configuration)
|
||||
* @param length the length of the component (if it implements IHaveLength), null otherwise
|
||||
*/
|
||||
public record ResizeDimensions(Size width, Size height, Size length) {
|
||||
}
|
||||
|
||||
@@ -46,11 +46,21 @@ public class Size extends AbstractMeasure<SizeUnit> implements Comparable<Size>
|
||||
return getValue() * getUnit().getFactor() / SizeUnit.in.getFactor() * Constants.PIXELS_PER_INCH;
|
||||
}
|
||||
|
||||
public double convertToUnits(SizeUnit unit) {
|
||||
/**
|
||||
* Converts this size to the given unit. Returns a new Size with the same
|
||||
* physical length expressed in the target unit.
|
||||
*/
|
||||
public Size convertToUnits(SizeUnit unit) {
|
||||
if (unit == getUnit()) {
|
||||
return this.getValue();
|
||||
return new Size(roundTo4Decimals(getValue()), unit);
|
||||
}
|
||||
return convertToPixels() * SizeUnit.px.getFactor();
|
||||
double lengthInBase = getValue() * getUnit().getFactor();
|
||||
double valueInTargetUnit = lengthInBase / unit.getFactor();
|
||||
return new Size(roundTo4Decimals(valueInTargetUnit), unit);
|
||||
}
|
||||
|
||||
private static double roundTo4Decimals(double value) {
|
||||
return Math.round(value * 10000) / 10000.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,18 +23,35 @@ package org.diylc.core.measures;
|
||||
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public enum SizeUnit implements Unit {
|
||||
|
||||
px(25.4d / Constants.PIXELS_PER_INCH), mm(1d), cm(10d), m(1e3d), in(25.4d), ft(25.4d * 12), yd(9144d);
|
||||
px(25.4d / Constants.PIXELS_PER_INCH, SizeUnitType.Digital),
|
||||
mm(1d, SizeUnitType.Metric),
|
||||
cm(10d, SizeUnitType.Metric),
|
||||
m(1e3d, SizeUnitType.Metric),
|
||||
in(25.4d, SizeUnitType.Imperial),
|
||||
ft(25.4d * 12, SizeUnitType.Imperial),
|
||||
yd(9144d, SizeUnitType.Imperial);
|
||||
|
||||
double factor;
|
||||
SizeUnitType type;
|
||||
|
||||
private SizeUnit(double factor) {
|
||||
private SizeUnit(double factor, SizeUnitType type) {
|
||||
this.factor = factor;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getFactor() {
|
||||
return factor;
|
||||
}
|
||||
|
||||
public SizeUnitType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public static final Map<SizeUnit, SizeUnit> ConversionMapping =
|
||||
Map.of(mm, in, cm, in, m, yd, in, mm, ft, cm, yd, m);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.diylc.core.measures;
|
||||
|
||||
public enum SizeUnitType {
|
||||
Metric, Imperial, Digital
|
||||
}
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.common;
|
||||
package org.diylc.filter;
|
||||
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
+1
-2
@@ -19,11 +19,10 @@
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
package org.diylc.filter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.diylc.common.IComponentFilter;
|
||||
import org.diylc.common.PCBLayer;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.diylc.filter;
|
||||
|
||||
import org.diylc.core.IBoard;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.gerber.GerberRenderMode;
|
||||
import org.diylc.core.gerber.IGerberComponent;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class SilkScreenFilter implements IComponentFilter {
|
||||
|
||||
@Override
|
||||
public boolean testComponent(IDIYComponent<?> component) {
|
||||
if (component instanceof IBoard board) {
|
||||
return board.shouldExportToGerber();
|
||||
}
|
||||
if (!(component instanceof IGerberComponent gerberComponent)) {
|
||||
return false;
|
||||
}
|
||||
Set<GerberRenderMode> gerberRenderModes = gerberComponent.getGerberRenderModes();
|
||||
return gerberRenderModes.contains(GerberRenderMode.Outline);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class NetlistBuilder {
|
||||
|
||||
public static final float eps = 4; // consider any nodes closer than this as connected
|
||||
|
||||
public static int MAX_SWITCH_COMBINATIONS = 256;
|
||||
public static int MAX_SWITCH_COMBINATIONS = 1024;
|
||||
|
||||
private static final String MAX_SWITCH_COMBINATIONS_ERROR = LangUtil
|
||||
.translate("Maximum number of switching combinations exceeded. Allowed: %s, actual: %s");
|
||||
@@ -72,6 +72,7 @@ public class NetlistBuilder {
|
||||
if (!(c instanceof IContinuity && !(c instanceof ISwitch)) && !(c instanceof ICommonNode)) {
|
||||
// regular components, create a node per control point
|
||||
for (int i = 0; i < c.getControlPointCount(); i++) {
|
||||
// Node renders the name through Node.sanitizeNodeName; here it only marks a named point
|
||||
String nodeName = c.getControlPointNodeName(i);
|
||||
if (c.isControlPointSticky(i) && nodeName != null
|
||||
&& (!includeSwitches || !ISwitch.class.isAssignableFrom(type.getInstanceClass()))) {
|
||||
@@ -410,6 +411,23 @@ public class NetlistBuilder {
|
||||
if (((IContinuity) c).arePointsConnected(i, j))
|
||||
connections.add(new Connection(c.getControlPoint(i), c.getControlPoint(j), z));
|
||||
}
|
||||
// Pins of one component that name the same rail are the same node: a board's eight ground
|
||||
// pins land on one plane, and grounding one of them must not leave the rest floating.
|
||||
// Switches are excluded because their connectivity depends on the position, which a name
|
||||
// cannot express -- a switch states it through ISwitch.arePointsConnected instead.
|
||||
if (!(c instanceof ISwitch)) {
|
||||
for (int i = 0; i < c.getControlPointCount() - 1; i++) {
|
||||
String rail = Node.railName(c.getControlPointNodeName(i));
|
||||
if (rail == null) {
|
||||
continue;
|
||||
}
|
||||
for (int j = i + 1; j < c.getControlPointCount(); j++) {
|
||||
if (rail.equals(Node.railName(c.getControlPointNodeName(j)))) {
|
||||
connections.add(new Connection(c.getControlPoint(i), c.getControlPoint(j), z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// handle switches
|
||||
if (ISwitch.class.isAssignableFrom(type.getInstanceClass())
|
||||
&& switchPositions.containsKey(c)) {
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
package org.diylc.netlist;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.diylc.core.ICommonNode;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -34,6 +37,10 @@ import org.diylc.core.IDIYComponent;
|
||||
*/
|
||||
public class Node implements Comparable<Node> {
|
||||
|
||||
/** Pin names that mark an absent connection, so repeats of them are not one net. */
|
||||
private static final Set<String> NON_NET_NAMES =
|
||||
new HashSet<String>(Arrays.asList("NC", "RSV"));
|
||||
|
||||
private IDIYComponent<?> component;
|
||||
private int pointIndex;
|
||||
|
||||
@@ -52,8 +59,51 @@ public class Node implements Comparable<Node> {
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
String[] section = component.getSectionNames(pointIndex);
|
||||
return (section == null || section.length > 1 ? "" : section[0] + ".") + component.getControlPointNodeName(pointIndex);
|
||||
return sanitizeNodeName(component.getControlPointNodeName(pointIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the parenthesized annotation off a raw control point name, so a netlist reads
|
||||
* <code>MCU1.D11</code> rather than <code>MCU1.D11 (~, MOSI)</code>. The bare name identifies the
|
||||
* pin; everything in parentheses documents its alternate functions and only adds noise here.
|
||||
*
|
||||
* <p>A trailing disambiguator such as the <code>_1</code> in <code>GND_1</code> is kept, so the
|
||||
* netlist can still say which pad a connection reached. Whether two such pins are the same
|
||||
* net is a separate question, answered by {@link #railName(String)}.
|
||||
*
|
||||
* @param nodeName raw name from {@link IDIYComponent#getControlPointNodeName(int)}
|
||||
* @return the sanitized name, or null if the raw name was null
|
||||
*/
|
||||
public static String sanitizeNodeName(String nodeName) {
|
||||
if (nodeName == null) {
|
||||
return null;
|
||||
}
|
||||
int parenIndex = nodeName.indexOf('(');
|
||||
String sanitized = (parenIndex == -1 ? nodeName : nodeName.substring(0, parenIndex)).trim();
|
||||
// Never let sanitizing leave a pin nameless
|
||||
return sanitized.isEmpty() ? nodeName.trim() : sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* The rail a pin belongs to: its sanitized name with a trailing <code>_1</code>, <code>_2</code>
|
||||
* disambiguator removed, so <code>GND_1</code> and <code>GND_2</code> report the same rail.
|
||||
*
|
||||
* <p>Only a numeric suffix is dropped. An underscore does three unrelated jobs across the
|
||||
* pinouts in this library: it disambiguates a repeated rail, it namespaces a connector's signals
|
||||
* (<code>ETH_TX-</code>, <code>USB_D+</code>), and it qualifies a genuinely separate net
|
||||
* (<code>MISO_16U2</code>, <code>3V3_EN</code>). Cutting at the first underscore instead would
|
||||
* report an Ethernet header as one rail.
|
||||
*
|
||||
* @param nodeName raw name from {@link IDIYComponent#getControlPointNodeName(int)}
|
||||
* @return the rail name, or null if the pin has no name or names no net at all
|
||||
*/
|
||||
public static String railName(String nodeName) {
|
||||
String sanitized = sanitizeNodeName(nodeName);
|
||||
if (sanitized == null || sanitized.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String rail = sanitized.replaceAll("_\\d+$", "");
|
||||
return NON_NET_NAMES.contains(rail) ? null : rail;
|
||||
}
|
||||
|
||||
public Point2D getPoint2D() {
|
||||
|
||||
@@ -234,7 +234,7 @@ public class Tree {
|
||||
|
||||
public Set<IDIYComponent<?>> extractComponents(Set<String> types) {
|
||||
Set<IDIYComponent<?>> res = new HashSet<IDIYComponent<?>>();
|
||||
if (leaf != null && types.contains(leaf.getComponent().getClass().getCanonicalName())) {
|
||||
if (leaf != null && (types == null || types.contains(leaf.getComponent().getClass().getCanonicalName()))) {
|
||||
res.add(leaf.getComponent());
|
||||
} else if (children != null) {
|
||||
for (Tree t : children) {
|
||||
|
||||
+2
-1
@@ -175,7 +175,8 @@ public class AiProjectBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiComponent(component.getName(), componentType.getName(),
|
||||
return new AiComponent(component.getName(),
|
||||
ComponentProcessor.getDisplayTypeName(component, componentType),
|
||||
component.getValue() == null ? null : component.getValue().toString(),
|
||||
fromPos, toPos, pos, terminals.isEmpty() ? null : terminals);
|
||||
}
|
||||
|
||||
+22
-12
@@ -38,6 +38,7 @@ import org.diylc.core.ISwitch;
|
||||
import org.diylc.netlist.*;
|
||||
import org.diylc.plugins.chatbot.model.*;
|
||||
import org.diylc.plugins.cloud.model.IServiceAPI;
|
||||
import org.diylc.plugins.cloud.service.CloudException;
|
||||
import org.diylc.plugins.cloud.service.NotLoggedInException;
|
||||
import org.diylc.presenter.ComponentProcessor;
|
||||
import org.diylc.presenter.ContinuityArea;
|
||||
@@ -62,6 +63,8 @@ public class ChatbotService {
|
||||
|
||||
private final static Logger LOG = Logger.getLogger(ChatbotService.class);
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
public static final String SERVER_ERROR_PLEASE_TRY_AGAIN = "Server error, please try again.";
|
||||
|
||||
static {
|
||||
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
}
|
||||
@@ -106,15 +109,15 @@ public class ChatbotService {
|
||||
|
||||
private IChatbotAPI getService() {
|
||||
if (service == null) {
|
||||
String serviceUrl =
|
||||
ConfigurationManager.getInstance().readString(IServiceAPI.URL_KEY, "http://www.diy-fever.com/diylc/api/v1/ai");
|
||||
String serviceUrl = System.getProperty("org.diylc.cloud.baseUrl",
|
||||
ConfigurationManager.getInstance().readString(IServiceAPI.URL_KEY, "http://www.diy-fever.com/diylc/api/v1")) + "/ai";
|
||||
ProxyFactory factory = new ProxyFactory(new PhpFlatProxy());
|
||||
service = factory.createProxy(IChatbotAPI.class, serviceUrl);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
public String promptChatbot(String prompt) throws NotLoggedInException {
|
||||
public String promptChatbot(String prompt) throws NotLoggedInException, CloudException {
|
||||
if (!plugInPort.getCloudService().isLoggedIn())
|
||||
throw new NotLoggedInException();
|
||||
|
||||
@@ -126,13 +129,13 @@ public class ChatbotService {
|
||||
File aiProjectFile = getAiProjectFile();
|
||||
|
||||
try {
|
||||
return getService().promptChatbot(
|
||||
plugInPort.getCloudService().getCurrentUsername(),
|
||||
String response = getService().promptChatbot(plugInPort.getCloudService().getCurrentUsername(),
|
||||
plugInPort.getCloudService().getCurrentToken(),
|
||||
plugInPort.getCloudService().getMachineId(),
|
||||
fileName,
|
||||
aiProjectFile,
|
||||
prompt);
|
||||
plugInPort.getCloudService().getMachineId(), fileName, aiProjectFile, prompt);
|
||||
return Optional.ofNullable(response).orElse(SERVER_ERROR_PLEASE_TRY_AGAIN);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Chatbot error", e);
|
||||
throw new CloudException(SERVER_ERROR_PLEASE_TRY_AGAIN);
|
||||
} finally {
|
||||
// Clean up the temp file immediately after use
|
||||
if (aiProjectFile != null && aiProjectFile.exists()) {
|
||||
@@ -141,7 +144,7 @@ public class ChatbotService {
|
||||
}
|
||||
}
|
||||
|
||||
public String analyzeCircuit() throws NotLoggedInException {
|
||||
public String analyzeCircuit() throws NotLoggedInException, CloudException {
|
||||
if (!plugInPort.getCloudService().isLoggedIn())
|
||||
throw new NotLoggedInException();
|
||||
|
||||
@@ -156,9 +159,15 @@ public class ChatbotService {
|
||||
String jsonResult = getService().analyzeCircuit(plugInPort.getCloudService().getCurrentUsername(),
|
||||
plugInPort.getCloudService().getCurrentToken(),
|
||||
plugInPort.getCloudService().getMachineId(), fileName, aiProjectFile);
|
||||
|
||||
if (jsonResult == null || jsonResult.trim().isEmpty()) {
|
||||
throw new CloudException(SERVER_ERROR_PLEASE_TRY_AGAIN);
|
||||
}
|
||||
|
||||
return JsonToHtmlConverter.convertToHtml(jsonResult);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (Exception e) {
|
||||
LOG.error("AI Analyzer error", e);
|
||||
throw new CloudException(SERVER_ERROR_PLEASE_TRY_AGAIN);
|
||||
} finally {
|
||||
// Clean up the temp file immediately after use
|
||||
if (aiProjectFile != null && aiProjectFile.exists()) {
|
||||
@@ -188,6 +197,7 @@ public class ChatbotService {
|
||||
- Coordinates are represented in pixels assuming 200px/in resolution.
|
||||
- Each component will either have a set of terminals that are connectable to other components or one or two points defining its position.
|
||||
- Netlists of connected terminals are defined in the 'nets' structure.
|
||||
- Connective components (wires, jumpers, traces, etc) are not part of the netlist, they are there to understand physical diagram but their conductive properties are already taken into account when constructing the netlist
|
||||
- Analyze labels - they could provide clues about nearby terminals and what they represent.
|
||||
""");
|
||||
if (aiProject.switches() != null && !aiProject.switches().isEmpty()) {
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ public class JsonToHtmlConverter {
|
||||
.append("</head>\n<body>\n");
|
||||
|
||||
// Process each section
|
||||
processSection(root, "analysis", html, "#008000");
|
||||
processSection(root, "critical", html, "#c00");
|
||||
processSection(root, "warnings", html, "#e65c00");
|
||||
processSection(root, "optimizations", html, "#0066c0");
|
||||
|
||||
@@ -86,8 +86,8 @@ public class CloudService {
|
||||
public CloudService(IPlugInPort plugInPort) {
|
||||
this.plugInPort = plugInPort;
|
||||
|
||||
serviceUrl = ConfigurationManager.getInstance().readString(IServiceAPI.URL_KEY,
|
||||
"http://www.diy-fever.com/diylc/api/v1");
|
||||
serviceUrl = System.getProperty("org.diylc.cloud.baseUrl",
|
||||
ConfigurationManager.getInstance().readString(IServiceAPI.URL_KEY, "http://www.diy-fever.com/diylc/api/v1"));
|
||||
ProxyFactory factory = new ProxyFactory(new PhpFlatProxy());
|
||||
service = factory.createProxy(IServiceAPI.class, serviceUrl);
|
||||
|
||||
@@ -104,12 +104,6 @@ public class CloudService {
|
||||
}
|
||||
|
||||
private IServiceAPI getService() {
|
||||
if (service == null) {
|
||||
serviceUrl =
|
||||
ConfigurationManager.getInstance().readString(IServiceAPI.URL_KEY, "http://www.diy-fever.com/diylc/api/v1");
|
||||
ProxyFactory factory = new ProxyFactory(new PhpFlatProxy());
|
||||
service = factory.createProxy(IServiceAPI.class, serviceUrl);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
+81
-275
@@ -22,6 +22,7 @@
|
||||
package org.diylc.plugins.compare.service;
|
||||
|
||||
import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.common.DummyView;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.netlist.Group;
|
||||
@@ -33,14 +34,15 @@ import org.diylc.plugins.compare.model.ComponentDiff;
|
||||
import org.diylc.plugins.compare.model.CompareResults;
|
||||
import org.diylc.presenter.Presenter;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CompareService {
|
||||
|
||||
@@ -53,6 +55,9 @@ public class CompareService {
|
||||
public CompareResults compareWith(File file) {
|
||||
Presenter presenter = new Presenter(new DummyView(), InMemoryConfigurationManager.getInstance());
|
||||
presenter.loadProjectFromFile(file.getAbsolutePath());
|
||||
|
||||
renderInMemory(presenter);
|
||||
|
||||
try {
|
||||
List<Netlist> otherNetlists = presenter.extractNetlists(false);
|
||||
List<Netlist> thisNetlists = plugInPort.extractNetlists(false);
|
||||
@@ -65,284 +70,85 @@ public class CompareService {
|
||||
}
|
||||
}
|
||||
|
||||
public CompareResults compare(Netlist netlist1, Netlist netlist2) {
|
||||
// Get all groups from both netlists
|
||||
Set<Group> groups1 = netlist1.getGroups();
|
||||
Set<Group> groups2 = netlist2.getGroups();
|
||||
|
||||
// Build lists of differences
|
||||
List<ConnectionDiff> connectionDiffs = new ArrayList<>();
|
||||
Set<ComponentDiff> componentDiffs = new HashSet<>();
|
||||
Set<String> addedComponentNames = new HashSet<>();
|
||||
|
||||
// Helper to get a unique string for a node
|
||||
java.util.function.Function<Node, String> nodeKey = node ->
|
||||
node.getComponent().getName() + ":" + node.getComponent().getControlPointNodeName(node.getPointIndex());
|
||||
|
||||
// Get all component names from both netlists for existence check
|
||||
Set<String> netlist1Components = netlist1.getComponents().stream()
|
||||
.map(comp -> comp.getName())
|
||||
.collect(Collectors.toSet());
|
||||
Set<String> netlist2Components = netlist2.getComponents().stream()
|
||||
.map(comp -> comp.getName())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// For each group in netlist1, check if it exists in netlist2
|
||||
for (Group group1 : groups1) {
|
||||
boolean groupFound = false;
|
||||
for (Group group2 : groups2) {
|
||||
if (areGroupsEqual(group1, group2)) {
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!groupFound) {
|
||||
// If group not found, check each component
|
||||
List<Node> nodes = new ArrayList<>(group1.getNodes());
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
Node node = nodes.get(i);
|
||||
String componentName = node.getComponent().getName();
|
||||
|
||||
// Check if this component exists in any group in netlist2
|
||||
boolean componentExistsInNetlist2 = false;
|
||||
for (Group group2 : groups2) {
|
||||
for (Node node2 : group2.getNodes()) {
|
||||
if (node2.getComponent().getName().equals(componentName)) {
|
||||
componentExistsInNetlist2 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (componentExistsInNetlist2) break;
|
||||
}
|
||||
|
||||
// Add component difference if it doesn't exist in any group in netlist2 and hasn't been added yet
|
||||
if (!componentExistsInNetlist2 && addedComponentNames.add(componentName)) {
|
||||
componentDiffs.add(new ComponentDiff(componentName, true));
|
||||
}
|
||||
|
||||
// Add connection differences for each pair of nodes
|
||||
for (int j = i + 1; j < nodes.size(); j++) {
|
||||
Node otherNode = nodes.get(j);
|
||||
connectionDiffs.add(new ConnectionDiff(
|
||||
node.getComponent().getName(),
|
||||
node.getComponent().getControlPointNodeName(node.getPointIndex()),
|
||||
otherNode.getComponent().getName(),
|
||||
otherNode.getComponent().getControlPointNodeName(otherNode.getPointIndex()),
|
||||
true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each group in netlist2, check if it exists in netlist1
|
||||
for (Group group2 : groups2) {
|
||||
boolean groupFound = false;
|
||||
for (Group group1 : groups1) {
|
||||
if (areGroupsEqual(group1, group2)) {
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!groupFound) {
|
||||
// If group not found, check each component
|
||||
List<Node> nodes = new ArrayList<>(group2.getNodes());
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
Node node = nodes.get(i);
|
||||
String componentName = node.getComponent().getName();
|
||||
|
||||
// Check if this component exists in any group in netlist1
|
||||
boolean componentExistsInNetlist1 = false;
|
||||
for (Group group1 : groups1) {
|
||||
for (Node node1 : group1.getNodes()) {
|
||||
if (node1.getComponent().getName().equals(componentName)) {
|
||||
componentExistsInNetlist1 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (componentExistsInNetlist1) break;
|
||||
}
|
||||
|
||||
// Add component difference if it doesn't exist in any group in netlist1 and hasn't been added yet
|
||||
if (!componentExistsInNetlist1 && addedComponentNames.add(componentName)) {
|
||||
componentDiffs.add(new ComponentDiff(componentName, false));
|
||||
}
|
||||
|
||||
// Add connection differences for each pair of nodes
|
||||
for (int j = i + 1; j < nodes.size(); j++) {
|
||||
Node otherNode = nodes.get(j);
|
||||
connectionDiffs.add(new ConnectionDiff(
|
||||
node.getComponent().getName(),
|
||||
node.getComponent().getControlPointNodeName(node.getPointIndex()),
|
||||
otherNode.getComponent().getName(),
|
||||
otherNode.getComponent().getControlPointNodeName(otherNode.getPointIndex()),
|
||||
false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to resolve differences by flipping non-polarized components
|
||||
if (!connectionDiffs.isEmpty()) {
|
||||
// Find all non-polarized components that have nodes in different groups
|
||||
Map<String, List<Group>> componentGroups = new HashMap<>();
|
||||
for (Group group : groups1) {
|
||||
for (Node node : group.getNodes()) {
|
||||
if (!node.getComponent().isPolarized()) {
|
||||
componentGroups.computeIfAbsent(node.getComponent().getName(), k -> new ArrayList<>())
|
||||
.add(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try swapping nodes for each non-polarized component that appears in multiple groups
|
||||
for (Map.Entry<String, List<Group>> entry : componentGroups.entrySet()) {
|
||||
if (entry.getValue().size() >= 2) {
|
||||
String componentName = entry.getKey();
|
||||
List<Group> groupsWithComponent = entry.getValue();
|
||||
|
||||
// Get the nodes from the first two groups
|
||||
Group group1 = groupsWithComponent.get(0);
|
||||
Group group2 = groupsWithComponent.get(1);
|
||||
|
||||
Node node1 = group1.getNodes().stream()
|
||||
.filter(n -> n.getComponent().getName().equals(componentName))
|
||||
.findFirst()
|
||||
.get();
|
||||
Node node2 = group2.getNodes().stream()
|
||||
.filter(n -> n.getComponent().getName().equals(componentName))
|
||||
.findFirst()
|
||||
.get();
|
||||
|
||||
// Create new groups with swapped nodes
|
||||
Group newGroup1 = new Group();
|
||||
Group newGroup2 = new Group();
|
||||
|
||||
// Copy all nodes except the ones we're swapping
|
||||
for (Node n : group1.getNodes()) {
|
||||
if (!n.getComponent().getName().equals(componentName)) {
|
||||
newGroup1.getNodes().add(n);
|
||||
}
|
||||
}
|
||||
for (Node n : group2.getNodes()) {
|
||||
if (!n.getComponent().getName().equals(componentName)) {
|
||||
newGroup2.getNodes().add(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the swapped nodes
|
||||
newGroup1.getNodes().add(node2);
|
||||
newGroup2.getNodes().add(node1);
|
||||
|
||||
// Create a new set of groups with the swapped nodes
|
||||
Set<Group> newGroups1 = new HashSet<>(groups1);
|
||||
newGroups1.remove(group1);
|
||||
newGroups1.remove(group2);
|
||||
newGroups1.add(newGroup1);
|
||||
newGroups1.add(newGroup2);
|
||||
|
||||
// Check if this resolves ALL differences
|
||||
List<ConnectionDiff> newDiffs = new ArrayList<>();
|
||||
Set<ComponentDiff> newComponentDiffs = new HashSet<>();
|
||||
|
||||
// Check groups from netlist1 against netlist2
|
||||
for (Group g1 : newGroups1) {
|
||||
boolean groupFound = false;
|
||||
for (Group g2 : groups2) {
|
||||
if (areGroupsEqual(g1, g2)) {
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!groupFound) {
|
||||
// Add connection differences for this group
|
||||
List<Node> nodes = new ArrayList<>(g1.getNodes());
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
Node node = nodes.get(i);
|
||||
for (int j = i + 1; j < nodes.size(); j++) {
|
||||
Node otherNode = nodes.get(j);
|
||||
newDiffs.add(new ConnectionDiff(
|
||||
node.getComponent().getName(),
|
||||
node.getComponent().getControlPointNodeName(node.getPointIndex()),
|
||||
otherNode.getComponent().getName(),
|
||||
otherNode.getComponent().getControlPointNodeName(otherNode.getPointIndex()),
|
||||
true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check groups from netlist2 against netlist1
|
||||
for (Group g2 : groups2) {
|
||||
boolean groupFound = false;
|
||||
for (Group g1 : newGroups1) {
|
||||
if (areGroupsEqual(g1, g2)) {
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!groupFound) {
|
||||
// Add connection differences for this group
|
||||
List<Node> nodes = new ArrayList<>(g2.getNodes());
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
Node node = nodes.get(i);
|
||||
for (int j = i + 1; j < nodes.size(); j++) {
|
||||
Node otherNode = nodes.get(j);
|
||||
newDiffs.add(new ConnectionDiff(
|
||||
node.getComponent().getName(),
|
||||
node.getComponent().getControlPointNodeName(node.getPointIndex()),
|
||||
otherNode.getComponent().getName(),
|
||||
otherNode.getComponent().getControlPointNodeName(otherNode.getPointIndex()),
|
||||
false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this swap resolved all differences, use the new groups
|
||||
if (newDiffs.isEmpty() && newComponentDiffs.isEmpty()) {
|
||||
groups1 = newGroups1;
|
||||
connectionDiffs = new ArrayList<>();
|
||||
componentDiffs = new HashSet<>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connectionDiffs.isEmpty() && componentDiffs.isEmpty()) {
|
||||
return new CompareResults(true, List.of(), List.of());
|
||||
}
|
||||
return new CompareResults(false, connectionDiffs, new ArrayList<>(componentDiffs));
|
||||
private void renderInMemory(Presenter presenter) {
|
||||
Dimension d = presenter.getCanvasDimensions(false, false);
|
||||
BufferedImage img = new BufferedImage(
|
||||
Math.max(1, d.width), Math.max(1, d.height), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
presenter.draw(g2d, EnumSet.noneOf(DrawOption.class), null, null, null, null);
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
private boolean areGroupsEqual(Group group1, Group group2) {
|
||||
if (group1.getNodes().size() != group2.getNodes().size()) {
|
||||
return false;
|
||||
public CompareResults compare(Netlist netlist1, Netlist netlist2) {
|
||||
Set<String> currentComponents = extractNetlistedComponents(netlist1);
|
||||
Set<String> targetComponents = extractNetlistedComponents(netlist2);
|
||||
|
||||
List<ComponentDiff> componentDiffs = new ArrayList<>();
|
||||
for (String name : currentComponents) {
|
||||
if (!targetComponents.contains(name)) {
|
||||
componentDiffs.add(new ComponentDiff(name, true));
|
||||
}
|
||||
}
|
||||
for (String name : targetComponents) {
|
||||
if (!currentComponents.contains(name)) {
|
||||
componentDiffs.add(new ComponentDiff(name, false));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get a unique string for a node
|
||||
java.util.function.Function<Node, String> nodeKey = node ->
|
||||
node.getComponent().getName() + ":" + node.getComponent().getControlPointNodeName(node.getPointIndex());
|
||||
Set<String> currentConnections = extractConnections(netlist1);
|
||||
Set<String> targetConnections = extractConnections(netlist2);
|
||||
|
||||
// For polarized components, we need to check exact node order
|
||||
List<String> group1Keys = group1.getNodes().stream()
|
||||
.map(nodeKey)
|
||||
.collect(Collectors.toList());
|
||||
List<String> group2Keys = group2.getNodes().stream()
|
||||
.map(nodeKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// If any component in the group is polarized, we need exact order match
|
||||
boolean hasPolarizedComponent = group1.getNodes().stream()
|
||||
.anyMatch(node -> node.getComponent().isPolarized()) ||
|
||||
group2.getNodes().stream()
|
||||
.anyMatch(node -> node.getComponent().isPolarized());
|
||||
|
||||
if (hasPolarizedComponent) {
|
||||
return group1Keys.equals(group2Keys);
|
||||
List<ConnectionDiff> connectionDiffs = new ArrayList<>();
|
||||
for (String conn : currentConnections) {
|
||||
if (!targetConnections.contains(conn)) {
|
||||
String[] parts = conn.split("\\|");
|
||||
if (targetComponents.contains(parts[0]) && targetComponents.contains(parts[2])) {
|
||||
connectionDiffs.add(new ConnectionDiff(parts[0], parts[1], parts[2], parts[3], true));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String conn : targetConnections) {
|
||||
if (!currentConnections.contains(conn)) {
|
||||
String[] parts = conn.split("\\|");
|
||||
if (currentComponents.contains(parts[0]) && currentComponents.contains(parts[2])) {
|
||||
connectionDiffs.add(new ConnectionDiff(parts[0], parts[1], parts[2], parts[3], false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For non-polarized components, we can use unordered comparison
|
||||
return new HashSet<>(group1Keys).equals(new HashSet<>(group2Keys));
|
||||
boolean matches = componentDiffs.isEmpty() && connectionDiffs.isEmpty();
|
||||
return new CompareResults(matches, connectionDiffs, componentDiffs);
|
||||
}
|
||||
|
||||
private Set<String> extractNetlistedComponents(Netlist netlist) {
|
||||
Set<String> names = new HashSet<>();
|
||||
for (Group group : netlist.getGroups()) {
|
||||
for (Node node : group.getNodes()) {
|
||||
names.add(node.getComponent().getName());
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private Set<String> extractConnections(Netlist netlist) {
|
||||
Set<String> connections = new HashSet<>();
|
||||
for (Group group : netlist.getGroups()) {
|
||||
List<Node> nodes = new ArrayList<>(group.getNodes());
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
for (int j = i + 1; j < nodes.size(); j++) {
|
||||
connections.add(connectionKey(nodes.get(i), nodes.get(j)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
private String connectionKey(Node a, Node b) {
|
||||
String keyA = a.getComponent().getName() + "|" + a.getComponent().getControlPointNodeName(a.getPointIndex());
|
||||
String keyB = b.getComponent().getName() + "|" + b.getComponent().getControlPointNodeName(b.getPointIndex());
|
||||
if (keyA.compareTo(keyB) <= 0) {
|
||||
return keyA + "|" + keyB;
|
||||
}
|
||||
return keyB + "|" + keyA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,8 @@
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||
|
||||
@@ -32,11 +27,15 @@ import com.thoughtworks.xstream.XStream;
|
||||
import com.thoughtworks.xstream.io.xml.DomDriver;
|
||||
import com.thoughtworks.xstream.security.AnyTypePermission;
|
||||
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.BuildingBlockPackage;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||
import org.diylc.components.composite.CompositeComponent;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.ISwitch;
|
||||
import org.diylc.serialization.ProjectFileManager;
|
||||
|
||||
public class BuildingBlockManager {
|
||||
@@ -124,19 +123,30 @@ public class BuildingBlockManager {
|
||||
return pkg.getBlocks().size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode {@link BlockInstantiationMode#GROUP} reproduces the original behavior: every
|
||||
* component is cloned individually, with a fresh project-unique name, ready to be
|
||||
* pasted as an auto-grouped set of independent components.
|
||||
* {@link BlockInstantiationMode#COMPOSITE} clones the components and wraps them in a
|
||||
* single {@link CompositeComponent}, returned as the only element of the result list.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<IDIYComponent<?>> loadBlock(String blockName,
|
||||
List<IDIYComponent<?>> existingComponents) throws InvalidBlockException {
|
||||
LOG.debug(String.format("loadBlock(%s)", blockName));
|
||||
List<IDIYComponent<?>> existingComponents, BlockInstantiationMode mode)
|
||||
throws InvalidBlockException {
|
||||
LOG.debug(String.format("loadBlock(%s, %s)", blockName, mode));
|
||||
Map<String, List<IDIYComponent<?>>> blocks = (Map<String, List<IDIYComponent<?>>>) configManager
|
||||
.readObject(IPlugInPort.BLOCKS_KEY, null);
|
||||
if (blocks != null) {
|
||||
Collection<IDIYComponent<?>> components = blocks.get(blockName);
|
||||
if (components == null)
|
||||
throw new InvalidBlockException();
|
||||
// clear potential control point every time!
|
||||
instantiationManager.setPotentialControlPoint(null);
|
||||
// clone components
|
||||
if (blocks == null)
|
||||
throw new InvalidBlockException();
|
||||
Collection<IDIYComponent<?>> components = blocks.get(blockName);
|
||||
if (components == null)
|
||||
throw new InvalidBlockException();
|
||||
// clear potential control point every time!
|
||||
instantiationManager.setPotentialControlPoint(null);
|
||||
|
||||
if (mode == BlockInstantiationMode.GROUP) {
|
||||
// clone components, each getting its own fresh, project-unique name and id
|
||||
List<IDIYComponent<?>> clones = new ArrayList<IDIYComponent<?>>();
|
||||
List<IDIYComponent<?>> testComponents = new ArrayList<IDIYComponent<?>>(existingComponents);
|
||||
for (IDIYComponent<?> c : components)
|
||||
@@ -145,14 +155,49 @@ public class BuildingBlockManager {
|
||||
clone.setName(instantiationManager.createUniqueName(ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) clone.getClass()),
|
||||
testComponents));
|
||||
clone.setId(UUID.randomUUID());
|
||||
testComponents.add(clone);
|
||||
clones.add(clone);
|
||||
} catch (CloneNotSupportedException e) {
|
||||
LOG.error("Could not clone component: " + c);
|
||||
}
|
||||
return clones;
|
||||
} else
|
||||
throw new InvalidBlockException();
|
||||
}
|
||||
|
||||
// COMPOSITE mode: clone components keeping their saved names. Those names surface in the
|
||||
// composite's terminal names when pins clash (e.g. "BLK1.R1.1"), so running them through
|
||||
// createUniqueName against the whole project would produce meaningless drift and make two
|
||||
// instances of the same block resolve different netlist labels.
|
||||
List<IDIYComponent<?>> childClones = new ArrayList<IDIYComponent<?>>();
|
||||
for (IDIYComponent<?> c : components) {
|
||||
try {
|
||||
IDIYComponent<?> clone = c.clone();
|
||||
clone.setId(UUID.randomUUID());
|
||||
childClones.add(clone);
|
||||
} catch (CloneNotSupportedException e) {
|
||||
LOG.error("Could not clone component: " + c);
|
||||
}
|
||||
// CompositeComponent does not implement ISwitch (see design decision D6): a switch placed
|
||||
// inside a composite still draws and exposes its terminals, but contributes no switching
|
||||
// behavior to the netlist. Flag it so this silent difference from group mode isn't a
|
||||
// surprise.
|
||||
if (c instanceof ISwitch) {
|
||||
LOG.warn(String.format(
|
||||
"Building block \"%s\" contains a switch (%s). Its switching behavior will not be "
|
||||
+ "reflected in the netlist when instantiated as a single component.",
|
||||
blockName, c.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
CompositeComponent composite = new CompositeComponent();
|
||||
composite.getChildComponents().addAll(childClones);
|
||||
composite.initBlockName(blockName);
|
||||
composite.setId(UUID.randomUUID());
|
||||
ComponentType compositeType =
|
||||
ComponentProcessor.getInstance().extractComponentTypeFrom(CompositeComponent.class);
|
||||
composite.setName(instantiationManager.createUniqueName(compositeType, existingComponents));
|
||||
|
||||
return Collections.singletonList((IDIYComponent<?>) composite);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -201,5 +246,11 @@ public class BuildingBlockManager {
|
||||
blocks.remove(blockName);
|
||||
configManager.writeValue(IBlockProcessor.BLOCKS_KEY, blocks);
|
||||
}
|
||||
|
||||
// drop the block from the recently used list too, so it doesn't linger as a dead entry
|
||||
List<String> recent =
|
||||
(List<String>) configManager.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
|
||||
if (recent != null && recent.remove(IBlockProcessor.BLOCK_PREFIX + blockName))
|
||||
configManager.writeValue(IPlugInPort.RECENT_COMPONENTS_KEY, recent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.List;
|
||||
|
||||
@@ -71,4 +72,54 @@ public class CalcUtils {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the relative luminance (perceived brightness) of a color.
|
||||
* Uses the standard formula: L = 0.299*R + 0.587*G + 0.114*B
|
||||
*
|
||||
* @param color the color to calculate luminance for
|
||||
* @return the luminance value between 0 and 255
|
||||
*/
|
||||
public static double calculateLuminance(Color color) {
|
||||
if (color == null) {
|
||||
return 0.0;
|
||||
}
|
||||
return 0.299 * color.getRed() + 0.587 * color.getGreen() + 0.114 * color.getBlue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a color to monochrome (black or white) based on its luminance.
|
||||
* Colors with luminance below the threshold (128) are converted to black,
|
||||
* otherwise to white. The alpha channel is preserved if present.
|
||||
*
|
||||
* @param color the color to convert
|
||||
* @param threshold the luminance threshold (default 128 for middle gray)
|
||||
* @return black or white color, preserving alpha channel
|
||||
*/
|
||||
public static Color convertToMonochrome(Color color, double threshold) {
|
||||
if (color == null) {
|
||||
return null;
|
||||
}
|
||||
double luminance = calculateLuminance(color);
|
||||
// Use <= to handle edge case where luminance equals threshold (should be white)
|
||||
// Add small epsilon to account for floating point precision issues
|
||||
Color monochromeColor = (luminance < threshold - 1e-10) ? Color.BLACK : Color.WHITE;
|
||||
// Preserve alpha channel if present
|
||||
if (color.getAlpha() < 255) {
|
||||
monochromeColor = new Color(monochromeColor.getRed(), monochromeColor.getGreen(),
|
||||
monochromeColor.getBlue(), color.getAlpha());
|
||||
}
|
||||
return monochromeColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a color to monochrome (black or white) based on its luminance.
|
||||
* Uses the default threshold of 128 (middle gray).
|
||||
*
|
||||
* @param color the color to convert
|
||||
* @return black or white color, preserving alpha channel
|
||||
*/
|
||||
public static Color convertToMonochrome(Color color) {
|
||||
return convertToMonochrome(color, 128.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import org.apache.log4j.Logger;
|
||||
@@ -35,6 +39,7 @@ import org.apache.log4j.Logger;
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.IComponentTransformer;
|
||||
import org.diylc.common.PropertyWrapper;
|
||||
import org.diylc.components.composite.CompositeComponent;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
@@ -63,6 +68,8 @@ public class ComponentProcessor {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ComponentProcessor.class);
|
||||
|
||||
private static final boolean LOG_COMPONENT_TYPE_DETAILS = false;
|
||||
|
||||
private static ComponentProcessor instance;
|
||||
|
||||
private Map<String, List<PropertyWrapper>> propertyCache;
|
||||
@@ -107,7 +114,7 @@ public class ComponentProcessor {
|
||||
String keywordTag;
|
||||
List<String[]> datasheet = null;
|
||||
int datasheetCreationStepCount = 0;
|
||||
|
||||
|
||||
if (clazz.isAnnotationPresent(ComponentDescriptor.class)) {
|
||||
ComponentDescriptor annotation = clazz.getAnnotation(ComponentDescriptor.class);
|
||||
name = annotation.name();
|
||||
@@ -153,7 +160,7 @@ public class ComponentProcessor {
|
||||
}
|
||||
ComponentType componentType =
|
||||
new ComponentType(name, description, creationMethod, category, namePrefix, author, icon, clazz, zOrder,
|
||||
flexibleZOrder, bomPolicy, autoEdit, transformer, keywordPolicy, keywordTag, cacheDrawing, datasheet,
|
||||
flexibleZOrder, bomPolicy, autoEdit, transformer, keywordPolicy, keywordTag, cacheDrawing, datasheet,
|
||||
datasheetCreationStepCount);
|
||||
componentTypeMap.put(clazz.getName(), componentType);
|
||||
return componentType;
|
||||
@@ -219,6 +226,18 @@ public class ComponentProcessor {
|
||||
return cloneProperties(properties);
|
||||
}
|
||||
|
||||
private static String formatPropertyType(Class<?> type) {
|
||||
return type == null ? null : type.getSimpleName();
|
||||
}
|
||||
|
||||
private static List<String> getPossibleValues(Class<?> type) {
|
||||
if (type == null || !type.isEnum())
|
||||
return null;
|
||||
return Arrays.stream(type.getEnumConstants())
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<PropertyWrapper> cloneProperties(List<PropertyWrapper> properties) {
|
||||
List<PropertyWrapper> result = new ArrayList<PropertyWrapper>(properties.size());
|
||||
for (PropertyWrapper propertyWrapper : properties) {
|
||||
@@ -243,6 +262,7 @@ public class ComponentProcessor {
|
||||
Set<Class<?>> componentTypeClasses = null;
|
||||
try {
|
||||
componentTypeClasses = reflections.getTypesAnnotatedWith(ComponentDescriptor.class, false);
|
||||
List<Map<String, Object>> componentSnapshots = LOG_COMPONENT_TYPE_DETAILS ? new ArrayList<>() : null;
|
||||
|
||||
for (Class<?> clazz : componentTypeClasses) {
|
||||
if (!Modifier.isAbstract(clazz.getModifiers()) && IDIYComponent.class.isAssignableFrom(clazz)) {
|
||||
@@ -254,6 +274,11 @@ public class ComponentProcessor {
|
||||
// just to store in the cache
|
||||
ComponentProcessor.getInstance().extractProperties(clazz);
|
||||
|
||||
// types created only programmatically (e.g. CompositeComponent) don't belong in the
|
||||
// palette, but still need to go through extractProperties() above so the cache is warm
|
||||
if (clazz.getAnnotation(ComponentDescriptor.class).hiddenInPalette())
|
||||
continue;
|
||||
|
||||
List<ComponentType> nestedList;
|
||||
if (componentTypes.containsKey(componentType.getCategory())) {
|
||||
nestedList = componentTypes.get(componentType.getCategory());
|
||||
@@ -262,6 +287,42 @@ public class ComponentProcessor {
|
||||
componentTypes.put(componentType.getCategory(), nestedList);
|
||||
}
|
||||
nestedList.add(componentType);
|
||||
|
||||
if (LOG_COMPONENT_TYPE_DETAILS) {
|
||||
try {
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("name", componentType.getName());
|
||||
snapshot.put("description", componentType.getDescription());
|
||||
snapshot.put("creationMethod", componentType.getCreationMethod());
|
||||
snapshot.put("category", componentType.getCategory());
|
||||
List<PropertyWrapper> props = propertyCache.get(clazz.getName());
|
||||
snapshot.put("properties", props != null ? props.stream()
|
||||
.map(p -> {
|
||||
Map<String, Object> prop = new LinkedHashMap<>();
|
||||
prop.put("name", p.getName());
|
||||
prop.put("type", formatPropertyType(p.getType()));
|
||||
List<String> possibleValues = getPossibleValues(p.getType());
|
||||
if (possibleValues != null) {
|
||||
prop.put("possibleValues", possibleValues);
|
||||
}
|
||||
return prop;
|
||||
})
|
||||
.collect(Collectors.toList()) : Collections.emptyList());
|
||||
componentSnapshots.add(snapshot);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to log component type details for " + clazz.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LOG_COMPONENT_TYPE_DETAILS && componentSnapshots != null && !componentSnapshots.isEmpty()) {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
LOG.info("Component types: " + mapper.writeValueAsString(componentSnapshots));
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to log component types JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +428,23 @@ public class ComponentProcessor {
|
||||
return transformer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name that should represent the component's type to a human or to the AI, e.g. in
|
||||
* the BOM, the netlist report or the AI project description. For a
|
||||
* {@link CompositeComponent} this is the source building block's name (e.g.
|
||||
* "Arduino Uno") rather than the generic "Building Block" type name, since every
|
||||
* composite shares one {@link ComponentType} (see design decision D5 in
|
||||
* {@code docs/plans/composite-building-blocks.md}).
|
||||
*/
|
||||
public static String getDisplayTypeName(IDIYComponent<?> component, ComponentType type) {
|
||||
if (component instanceof CompositeComponent) {
|
||||
String blockName = ((CompositeComponent) component).getBlockName();
|
||||
if (blockName != null && !blockName.isEmpty())
|
||||
return blockName;
|
||||
}
|
||||
return type.getName();
|
||||
}
|
||||
|
||||
public static boolean hasStickyPoint(IDIYComponent<?> c) {
|
||||
for (int i = 0; i < c.getControlPointCount(); i++)
|
||||
if (c.isControlPointSticky(0))
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
@@ -38,7 +39,7 @@ public class DatasheetService {
|
||||
|
||||
private static DatasheetService instance;
|
||||
|
||||
private static Map<String, List<String[]>> datasheetCache = new HashMap<String, List<String[]>>();
|
||||
private static Map<String, List<String[]>> datasheetCache = new ConcurrentHashMap<>();
|
||||
private static Map<String, Map<String, List<String[]>>> lookupCache =
|
||||
new HashMap<String, Map<String, List<String[]>>>();
|
||||
|
||||
|
||||
@@ -26,10 +26,12 @@ import java.lang.ref.SoftReference;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.core.ComponentForRender;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -67,14 +69,14 @@ public class DrawingCache {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void bulkPrepare(Collection<ComponentForRender> components, G2DWrapper g2d, boolean outlineMode, Project project,
|
||||
double zoom, double scaleFactor) {
|
||||
double zoom, double scaleFactor, Set<DrawOption> drawOptions) {
|
||||
// run all renders in parallel to utilize all CPU cores
|
||||
components.stream().parallel().forEach(x -> {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) x.getComponent().getClass());
|
||||
|
||||
if (type != null && type.getEnableCache()) {
|
||||
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder());
|
||||
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder(), drawOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -92,7 +94,7 @@ public class DrawingCache {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void draw(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Rectangle2D visibleRect) {
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Rectangle2D visibleRect, Set<DrawOption> drawOptions) {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
|
||||
@@ -100,7 +102,7 @@ public class DrawingCache {
|
||||
// if we need to apply caching
|
||||
Point2D firstPoint = component.getControlPoint(0);
|
||||
|
||||
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder);
|
||||
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder, drawOptions);
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
@@ -180,7 +182,7 @@ public class DrawingCache {
|
||||
}
|
||||
|
||||
private CacheValue renderAndCache(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder) {
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Set<DrawOption> drawOptions) {
|
||||
|
||||
if (!g2d.getClipBounds().intersects(component.getCachingBounds())) {
|
||||
return null;
|
||||
@@ -199,9 +201,11 @@ public class DrawingCache {
|
||||
}
|
||||
|
||||
// only honor the cache if the component hasn't changed in the meantime
|
||||
// Also check if drawOptions changed (e.g., MONOCHROMATIC affects rendering)
|
||||
boolean drawOptionsChanged = value == null || !value.getDrawOptions().equals(drawOptions);
|
||||
if (value == null || !value.getComponent().equalsTo(component)
|
||||
|| value.getState() != componentState || value.getZoom() != zoom || value.getOutlineMode() != outlineMode
|
||||
|| value.getScaleFactor() != scaleFactor) {
|
||||
|| value.getScaleFactor() != scaleFactor || drawOptionsChanged) {
|
||||
// figure out size and placement of buffer image
|
||||
Rectangle2D rect = component.getCachingBounds();
|
||||
int width = (int)Math.round(rect.getWidth() * zoom);
|
||||
@@ -233,7 +237,7 @@ public class DrawingCache {
|
||||
cg2d.translate(-firstPoint.getX() + dx, -firstPoint.getY() + dy);
|
||||
|
||||
// wrap the graphics so we can track
|
||||
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom);
|
||||
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom, drawOptions);
|
||||
|
||||
// initialize wrapper
|
||||
wrapper.startedDrawingComponent(zOrder);
|
||||
@@ -275,7 +279,7 @@ public class DrawingCache {
|
||||
|
||||
// add to the cache
|
||||
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
|
||||
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect);
|
||||
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect, drawOptions);
|
||||
imageCache.put(component, new SoftReference<DrawingCache.CacheValue>(value));
|
||||
}
|
||||
|
||||
@@ -295,11 +299,12 @@ public class DrawingCache {
|
||||
int dx;
|
||||
int dy;
|
||||
Rectangle2D cacheBounds;
|
||||
Set<DrawOption> drawOptions;
|
||||
|
||||
public CacheValue(IDIYComponent<?> component, BufferedImage image, Area currentArea,
|
||||
Map<String, Area> continuityPositiveAreas, Map<String, Area> continuityNegativeAreas,
|
||||
ComponentState state, boolean outlineMode, double zoom, double scaleFactor, int dx, int dy,
|
||||
Rectangle2D cacheBounds) {
|
||||
Rectangle2D cacheBounds, Set<DrawOption> drawOptions) {
|
||||
super();
|
||||
this.cacheBounds = cacheBounds;
|
||||
// take a copy of the component, so we can check if it changed in the meantime
|
||||
@@ -317,6 +322,7 @@ public class DrawingCache {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this.scaleFactor = scaleFactor;
|
||||
this.drawOptions = drawOptions;
|
||||
}
|
||||
|
||||
public IDIYComponent<?> getComponent() {
|
||||
@@ -366,6 +372,10 @@ public class DrawingCache {
|
||||
public Rectangle2D getCacheBounds() {
|
||||
return cacheBounds;
|
||||
}
|
||||
|
||||
public Set<DrawOption> getDrawOptions() {
|
||||
return drawOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -50,10 +50,10 @@ import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||
import org.diylc.appframework.simplemq.MessageDispatcher;
|
||||
|
||||
import org.diylc.common.*;
|
||||
import org.diylc.components.AbstractTransparentComponent;
|
||||
import org.diylc.core.*;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.filter.IComponentFilter;
|
||||
import org.diylc.netlist.ContinuityGraph;
|
||||
import org.diylc.netlist.NetlistBuilder;
|
||||
import org.diylc.utils.Constants;
|
||||
@@ -67,7 +67,7 @@ import org.diylc.utils.Constants;
|
||||
public class DrawingManager {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DrawingManager.class);
|
||||
public static final byte MIRROR_ALPHA = 64;
|
||||
public static final Percentage MIRROR_ALPHA = new Percentage(50);
|
||||
|
||||
public static int CONTROL_POINT_SIZE = 7;
|
||||
public static double EXTRA_SPACE = 0.25;
|
||||
@@ -183,7 +183,7 @@ public class DrawingManager {
|
||||
if (externalZoom != null)
|
||||
zoom *= externalZoom;
|
||||
|
||||
G2DWrapper g2dWrapper = new G2DWrapper(g2d, zoom);
|
||||
G2DWrapper g2dWrapper = new G2DWrapper(g2d, zoom, drawOptions);
|
||||
|
||||
configureRenderingHints(g2d, drawOptions);
|
||||
|
||||
@@ -191,19 +191,27 @@ public class DrawingManager {
|
||||
Map<IBoard, List<IDIYComponent<?>>> boardMap = project.getComponents().stream()
|
||||
.filter(IBoard.class::isInstance)
|
||||
.map(IBoard.class::cast)
|
||||
.filter(b -> b.getUndersideDisplay() != BoardUndersideDisplay.NONE)
|
||||
.filter(b -> b.getUndersideDisplay() != null && b.getUndersideDisplay() != BoardUndersideDisplay.NONE)
|
||||
.collect(Collectors.toMap(x -> x, x -> new ArrayList<IDIYComponent<?>>()));
|
||||
Map<IDIYComponent<?>, Set<IBoard>> componentBoardMap = new HashMap<IDIYComponent<?>, Set<IBoard>>();
|
||||
|
||||
for (IBoard board : boardMap.keySet()) {
|
||||
Rectangle2D boardRect = board.getBoardRectangle();
|
||||
if (boardRect == null) {
|
||||
continue;
|
||||
}
|
||||
double minX = Math.min(boardRect.getMinX(), boardRect.getMaxX());
|
||||
double maxX = Math.max(boardRect.getMinX(), boardRect.getMaxX());
|
||||
double minY = Math.min(boardRect.getMinY(), boardRect.getMaxY());
|
||||
double maxY = Math.max(boardRect.getMinY(), boardRect.getMaxY());
|
||||
Rectangle2D normalizedBoardRect = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
|
||||
for (IDIYComponent<?> c : project.getComponents()) {
|
||||
if (c == board) {
|
||||
continue;
|
||||
}
|
||||
boolean include = false;
|
||||
for (int i = 0; i < c.getControlPointCount(); i++) {
|
||||
if (boardRect.contains(c.getControlPoint(i))) {
|
||||
if (normalizedBoardRect.contains(c.getControlPoint(i))) {
|
||||
include = true;
|
||||
break;
|
||||
}
|
||||
@@ -267,7 +275,7 @@ public class DrawingManager {
|
||||
|
||||
// give the cache a chance to prepare all the components in multiple threads
|
||||
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) {
|
||||
DrawingCache.Instance.bulkPrepare(componentForRenderList, g2dWrapper, outlineMode, project, zoom, scaleFactor);
|
||||
DrawingCache.Instance.bulkPrepare(componentForRenderList, g2dWrapper, outlineMode, project, zoom, scaleFactor, drawOptions);
|
||||
}
|
||||
|
||||
// componentAreaMap.clear();
|
||||
@@ -336,26 +344,17 @@ public class DrawingManager {
|
||||
try {
|
||||
// Mirror components that belong to boards that need to be mirrored
|
||||
if (componentBoardMap.containsKey(component)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
ComponentType componentType = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
if (componentType.getTransformer() != null) {
|
||||
Set<IBoard> boards = componentBoardMap.get(component);
|
||||
for (IBoard board : boards) {
|
||||
drawMirroredComponent(project, board, component, componentType, g2dWrapper, state,
|
||||
outlineMode);
|
||||
}
|
||||
Set<IBoard> boards = componentBoardMap.get(component);
|
||||
for (IBoard board : boards) {
|
||||
drawMirroredComponent(project, board, component, g2dWrapper, state, outlineMode);
|
||||
}
|
||||
}
|
||||
|
||||
// if a component itself is a board that needs to be mirrored, do it
|
||||
if (IBoard.class.isInstance(component) && componentBoardMap.values().stream()
|
||||
.anyMatch(boards -> boards.contains(component))) {
|
||||
ComponentType componentType = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
if (componentType.getTransformer() != null) {
|
||||
drawMirroredComponent(project, (IBoard) component, component, componentType,
|
||||
g2dWrapper, state, outlineMode);
|
||||
if (component instanceof IBoard) {
|
||||
IBoard board = (IBoard) component;
|
||||
if (board.getUndersideDisplay() != null && board.getUndersideDisplay() != BoardUndersideDisplay.NONE) {
|
||||
drawMirroredComponent(project, board, component, g2dWrapper, state, outlineMode);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -385,8 +384,8 @@ public class DrawingManager {
|
||||
g2dWrapper.setColor(CONTROL_POINT_COLOR);
|
||||
Point2D controlPoint = component.getControlPoint(i);
|
||||
int pointSize = CONTROL_POINT_SIZE - 2;
|
||||
g2dWrapper.fillOval((int) (controlPoint.getX() - pointSize / 2),
|
||||
(int) (controlPoint.getY() - pointSize / 2), pointSize, pointSize);
|
||||
g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - pointSize / 2),
|
||||
(int) Math.round(controlPoint.getY() - pointSize / 2), pointSize, pointSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,11 +401,11 @@ public class DrawingManager {
|
||||
int pointSize = CONTROL_POINT_SIZE;
|
||||
|
||||
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR.darker());
|
||||
g2dWrapper.fillOval((int) (controlPoint.getX() - pointSize / 2),
|
||||
(int) (controlPoint.getY() - pointSize / 2), pointSize, pointSize);
|
||||
g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - pointSize / 2),
|
||||
(int) Math.round(controlPoint.getY() - pointSize / 2), pointSize, pointSize);
|
||||
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR);
|
||||
g2dWrapper.fillOval((int) (controlPoint.getX() - CONTROL_POINT_SIZE / 2 + 1),
|
||||
(int) (controlPoint.getY() - CONTROL_POINT_SIZE / 2 + 1), CONTROL_POINT_SIZE - 2,
|
||||
g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - CONTROL_POINT_SIZE / 2 + 1),
|
||||
(int) Math.round(controlPoint.getY() - CONTROL_POINT_SIZE / 2 + 1), CONTROL_POINT_SIZE - 2,
|
||||
CONTROL_POINT_SIZE - 2);
|
||||
}
|
||||
}
|
||||
@@ -436,12 +435,12 @@ public class DrawingManager {
|
||||
for (Point2D point : controlPointSlot) {
|
||||
if (point != null) {
|
||||
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR.darker());
|
||||
g2dWrapper.fillOval((int) (point.getX() - CONTROL_POINT_SIZE / 2),
|
||||
(int) (point.getY() - CONTROL_POINT_SIZE / 2), CONTROL_POINT_SIZE,
|
||||
g2dWrapper.fillOval((int) Math.round(point.getX() - CONTROL_POINT_SIZE / 2),
|
||||
(int) Math.round(point.getY() - CONTROL_POINT_SIZE / 2), CONTROL_POINT_SIZE,
|
||||
CONTROL_POINT_SIZE);
|
||||
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR);
|
||||
g2dWrapper.fillOval((int) (point.getX() - CONTROL_POINT_SIZE / 2 + 1),
|
||||
(int) (point.getY() - CONTROL_POINT_SIZE / 2 + 1), CONTROL_POINT_SIZE - 2,
|
||||
g2dWrapper.fillOval((int) Math.round(point.getX() - CONTROL_POINT_SIZE / 2 + 1),
|
||||
(int) Math.round(point.getY() - CONTROL_POINT_SIZE / 2 + 1), CONTROL_POINT_SIZE - 2,
|
||||
CONTROL_POINT_SIZE - 2);
|
||||
}
|
||||
}
|
||||
@@ -525,46 +524,94 @@ public class DrawingManager {
|
||||
}
|
||||
|
||||
private static void drawMirroredComponent(Project project, IBoard board, IDIYComponent<?> component,
|
||||
ComponentType componentType, G2DWrapper g2dWrapper, ComponentState state, boolean outlineMode) {
|
||||
double offset = board.getUndersideOffset().convertToPixels();
|
||||
G2DWrapper g2dWrapper, ComponentState state, boolean outlineMode) {
|
||||
if (board.getUndersideDisplay() == null || board.getUndersideDisplay() == BoardUndersideDisplay.NONE) {
|
||||
return;
|
||||
}
|
||||
double offset = board.getUndersideOffset() == null ? 0 : board.getUndersideOffset().convertToPixels();
|
||||
Rectangle2D boardRectangle = board.getBoardRectangle();
|
||||
try {
|
||||
IDIYComponent<?> clonedComponent = component.clone();
|
||||
int direction = 0;
|
||||
Point2D pivotPoint = new Point2D.Double();
|
||||
switch (board.getUndersideDisplay()) {
|
||||
case ABOVE:
|
||||
direction = IComponentTransformer.VERTICAL;
|
||||
pivotPoint.setLocation(boardRectangle.getMinX(), boardRectangle.getMinY() - offset / 2);
|
||||
break;
|
||||
case BELOW:
|
||||
direction = IComponentTransformer.VERTICAL;
|
||||
pivotPoint.setLocation(boardRectangle.getMinX(), boardRectangle.getMaxY() + offset / 2);
|
||||
break;
|
||||
case LEFT:
|
||||
direction = IComponentTransformer.HORIZONTAL;
|
||||
pivotPoint.setLocation(boardRectangle.getMinX() - offset / 2, boardRectangle.getMinY());
|
||||
break;
|
||||
case RIGHT:
|
||||
direction = IComponentTransformer.HORIZONTAL;
|
||||
pivotPoint.setLocation(boardRectangle.getMaxX() + offset / 2, boardRectangle.getMinY());
|
||||
break;
|
||||
}
|
||||
componentType.getTransformer().mirror(clonedComponent, pivotPoint, direction);
|
||||
if (board.getUndersideTransparency() && AbstractTransparentComponent.class.isInstance(clonedComponent)) {
|
||||
AbstractTransparentComponent<?> transparentComponent = (AbstractTransparentComponent<?>) clonedComponent;
|
||||
transparentComponent.setAlpha(MIRROR_ALPHA);
|
||||
if (boardRectangle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double minX = Math.min(boardRectangle.getMinX(), boardRectangle.getMaxX());
|
||||
double maxX = Math.max(boardRectangle.getMinX(), boardRectangle.getMaxX());
|
||||
double minY = Math.min(boardRectangle.getMinY(), boardRectangle.getMaxY());
|
||||
double maxY = Math.max(boardRectangle.getMinY(), boardRectangle.getMaxY());
|
||||
|
||||
AffineTransform mirrorTx = new AffineTransform();
|
||||
switch (board.getUndersideDisplay()) {
|
||||
case ABOVE: {
|
||||
double pivotY = minY - offset / 2;
|
||||
mirrorTx.translate(0, pivotY);
|
||||
mirrorTx.scale(1.0, -1.0);
|
||||
mirrorTx.translate(0, -pivotY);
|
||||
break;
|
||||
}
|
||||
clonedComponent.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new RuntimeException(e);
|
||||
case BELOW: {
|
||||
double pivotY = maxY + offset / 2;
|
||||
mirrorTx.translate(0, pivotY);
|
||||
mirrorTx.scale(1.0, -1.0);
|
||||
mirrorTx.translate(0, -pivotY);
|
||||
break;
|
||||
}
|
||||
case LEFT: {
|
||||
double pivotX = minX - offset / 2;
|
||||
mirrorTx.translate(pivotX, 0);
|
||||
mirrorTx.scale(-1.0, 1.0);
|
||||
mirrorTx.translate(-pivotX, 0);
|
||||
break;
|
||||
}
|
||||
case RIGHT: {
|
||||
double pivotX = maxX + offset / 2;
|
||||
mirrorTx.translate(pivotX, 0);
|
||||
mirrorTx.scale(-1.0, 1.0);
|
||||
mirrorTx.translate(-pivotX, 0);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
G2DWrapper.MirrorDirection mirrorDirection;
|
||||
switch (board.getUndersideDisplay()) {
|
||||
case ABOVE:
|
||||
case BELOW:
|
||||
mirrorDirection = G2DWrapper.MirrorDirection.VERTICAL;
|
||||
break;
|
||||
case LEFT:
|
||||
case RIGHT:
|
||||
mirrorDirection = G2DWrapper.MirrorDirection.HORIZONTAL;
|
||||
break;
|
||||
default:
|
||||
mirrorDirection = G2DWrapper.MirrorDirection.NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
AffineTransform oldTx = g2dWrapper.getTransform();
|
||||
Composite oldComposite = g2dWrapper.getComposite();
|
||||
g2dWrapper.setMirrorDirection(mirrorDirection);
|
||||
try {
|
||||
g2dWrapper.transform(mirrorTx);
|
||||
if (Boolean.TRUE.equals(board.getUndersideTransparency())) {
|
||||
g2dWrapper.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)(MIRROR_ALPHA.getValue().doubleValue() / 100d)));
|
||||
}
|
||||
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error drawing mirrored " + component.getName(), e);
|
||||
} finally {
|
||||
g2dWrapper.setMirrorDirection(G2DWrapper.MirrorDirection.NONE);
|
||||
g2dWrapper.setTransform(oldTx);
|
||||
g2dWrapper.setComposite(oldComposite);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor, Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state, double zoom, int i, boolean outlineMode) {
|
||||
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor,
|
||||
Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state,
|
||||
double zoom, int i, boolean outlineMode) {
|
||||
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache
|
||||
DrawingCache.Instance.draw(component, g2dWrapper, state,
|
||||
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect);
|
||||
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect, drawOptions);
|
||||
else // go straight to the wrapper
|
||||
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.awt.Shape;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.font.FontRenderContext;
|
||||
import java.awt.font.GlyphVector;
|
||||
import java.awt.font.TextLayout;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Arc2D;
|
||||
import java.awt.geom.Area;
|
||||
@@ -53,17 +54,11 @@ import java.awt.image.renderable.RenderableImage;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.text.AttributedCharacterIterator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.common.ZoomableStroke;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
@@ -71,7 +66,7 @@ import org.diylc.core.IDrawingObserver;
|
||||
/**
|
||||
* {@link Graphics2D} wrapper that keeps track of all drawing actions and creates an {@link Area}
|
||||
* that corresponds to drawn objects. Before each component is drawn,
|
||||
* {@link #startedDrawingComponent()} should be called. After the component is drawn, area may be
|
||||
* {@link #startedDrawingComponent(int)} should be called. After the component is drawn, area may be
|
||||
* retrieved using {@link #finishedDrawingComponent()}. Graphics configuration (color, font, etc) is
|
||||
* reset between each two components.
|
||||
*
|
||||
@@ -96,7 +91,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
private int uniqueCounter = 0;
|
||||
|
||||
private Graphics2D canvasGraphics;
|
||||
private final Graphics2D canvasGraphics;
|
||||
private final Set<DrawOption> drawOptions;
|
||||
|
||||
private Stroke originalStroke;
|
||||
private Color originalColor;
|
||||
private Composite originalComposite;
|
||||
@@ -110,17 +107,25 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
private String continuityMarker;
|
||||
private Shape lastShape;
|
||||
|
||||
public enum MirrorDirection {
|
||||
NONE, HORIZONTAL, VERTICAL
|
||||
}
|
||||
|
||||
private MirrorDirection mirrorDirection = MirrorDirection.NONE;
|
||||
|
||||
private double zoom;
|
||||
private int zOrder;
|
||||
|
||||
/**
|
||||
* Creates a wrapper around specified {@link Graphics2D} object.
|
||||
*
|
||||
*
|
||||
* @param canvasGraphics
|
||||
* @param drawOptions
|
||||
*/
|
||||
public G2DWrapper(Graphics2D canvasGraphics, double zoom) {
|
||||
public G2DWrapper(Graphics2D canvasGraphics, double zoom, Set<DrawOption> drawOptions) {
|
||||
super();
|
||||
this.canvasGraphics = canvasGraphics;
|
||||
this.drawOptions = drawOptions;
|
||||
String debugAreaPerformanceStr = System.getProperty(DEBUG_AREA_PERFORMANCE);
|
||||
debugAreaPerformance = "true".equalsIgnoreCase(debugAreaPerformanceStr);
|
||||
this.zoom = zoom;
|
||||
@@ -135,6 +140,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
return canvasGraphics;
|
||||
}
|
||||
|
||||
public void setMirrorDirection(MirrorDirection mirrorDirection) {
|
||||
this.mirrorDirection = mirrorDirection != null ? mirrorDirection : MirrorDirection.NONE;
|
||||
}
|
||||
|
||||
public MirrorDirection getMirrorDirection() {
|
||||
return mirrorDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears out the current area and caches canvas settings.
|
||||
*
|
||||
@@ -334,7 +347,35 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
@Override
|
||||
public void drawGlyphVector(GlyphVector g, float x, float y) {
|
||||
canvasGraphics.drawGlyphVector(g, x, y);
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
if (mirrorDirection != MirrorDirection.NONE) {
|
||||
Rectangle2D bounds = g.getVisualBounds();
|
||||
double width = bounds.getWidth();
|
||||
double height = bounds.getHeight();
|
||||
AffineTransform oldTx = canvasGraphics.getTransform();
|
||||
AffineTransform flipTx = new AffineTransform();
|
||||
if (mirrorDirection == MirrorDirection.HORIZONTAL) {
|
||||
double cx = x + bounds.getX() + width / 2.0;
|
||||
flipTx.translate(cx, 0);
|
||||
flipTx.scale(-1.0, 1.0);
|
||||
flipTx.translate(-cx, 0);
|
||||
} else if (mirrorDirection == MirrorDirection.VERTICAL) {
|
||||
double cy = y + bounds.getY() + height / 2.0;
|
||||
flipTx.translate(0, cy);
|
||||
flipTx.scale(1.0, -1.0);
|
||||
flipTx.translate(0, -cy);
|
||||
}
|
||||
canvasGraphics.transform(flipTx);
|
||||
try {
|
||||
canvasGraphics.drawGlyphVector(g, x, y);
|
||||
} finally {
|
||||
canvasGraphics.setTransform(oldTx);
|
||||
}
|
||||
} else {
|
||||
canvasGraphics.drawGlyphVector(g, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -363,7 +404,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
@Override
|
||||
public void drawString(String str, int x, int y) {
|
||||
canvasGraphics.drawString(str, x, y);
|
||||
if (str == null) {
|
||||
return;
|
||||
}
|
||||
if (mirrorDirection != MirrorDirection.NONE) {
|
||||
drawMirroredString(str, x, y);
|
||||
} else {
|
||||
canvasGraphics.drawString(str, x, y);
|
||||
}
|
||||
if (drawingComponent && trackingAllowed) {
|
||||
FontMetrics fontMetrics = canvasGraphics.getFontMetrics();
|
||||
Rectangle2D rect = fontMetrics.getStringBounds(str, canvasGraphics);
|
||||
@@ -378,7 +426,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
@Override
|
||||
public void drawString(String str, float x, float y) {
|
||||
canvasGraphics.drawString(str, x, y);
|
||||
if (str == null) {
|
||||
return;
|
||||
}
|
||||
if (mirrorDirection != MirrorDirection.NONE) {
|
||||
drawMirroredString(str, x, y);
|
||||
} else {
|
||||
canvasGraphics.drawString(str, x, y);
|
||||
}
|
||||
if (drawingComponent && trackingAllowed) {
|
||||
FontMetrics fontMetrics = canvasGraphics.getFontMetrics();
|
||||
Rectangle2D rect = fontMetrics.getStringBounds(str, canvasGraphics);
|
||||
@@ -388,14 +443,87 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
@Override
|
||||
public void drawString(AttributedCharacterIterator iterator, int x, int y) {
|
||||
canvasGraphics.drawString(iterator, x, y);
|
||||
// FIXME: process map
|
||||
if (iterator == null) {
|
||||
return;
|
||||
}
|
||||
if (mirrorDirection != MirrorDirection.NONE) {
|
||||
drawMirroredString(iterator, x, y);
|
||||
} else {
|
||||
canvasGraphics.drawString(iterator, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawString(AttributedCharacterIterator iterator, float x, float y) {
|
||||
canvasGraphics.drawString(iterator, x, y);
|
||||
// FIXME: process map
|
||||
if (iterator == null) {
|
||||
return;
|
||||
}
|
||||
if (mirrorDirection != MirrorDirection.NONE) {
|
||||
drawMirroredString(iterator, x, y);
|
||||
} else {
|
||||
canvasGraphics.drawString(iterator, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawMirroredString(String str, float x, float y) {
|
||||
FontMetrics fontMetrics = canvasGraphics.getFontMetrics();
|
||||
Rectangle2D rect = fontMetrics.getStringBounds(str, canvasGraphics);
|
||||
double width = rect.getWidth();
|
||||
double ascent = fontMetrics.getAscent();
|
||||
double descent = fontMetrics.getDescent();
|
||||
|
||||
AffineTransform oldTx = canvasGraphics.getTransform();
|
||||
AffineTransform flipTx = new AffineTransform();
|
||||
|
||||
if (mirrorDirection == MirrorDirection.HORIZONTAL) {
|
||||
double cx = x + width / 2.0;
|
||||
flipTx.translate(cx, 0);
|
||||
flipTx.scale(-1.0, 1.0);
|
||||
flipTx.translate(-cx, 0);
|
||||
} else if (mirrorDirection == MirrorDirection.VERTICAL) {
|
||||
double cy = y - (ascent - descent) / 2.0;
|
||||
flipTx.translate(0, cy);
|
||||
flipTx.scale(1.0, -1.0);
|
||||
flipTx.translate(0, -cy);
|
||||
}
|
||||
|
||||
canvasGraphics.transform(flipTx);
|
||||
try {
|
||||
canvasGraphics.drawString(str, x, y);
|
||||
} finally {
|
||||
canvasGraphics.setTransform(oldTx);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawMirroredString(AttributedCharacterIterator iterator, float x, float y) {
|
||||
FontRenderContext frc = canvasGraphics.getFontRenderContext();
|
||||
TextLayout layout = new TextLayout(iterator, frc);
|
||||
Rectangle2D bounds = layout.getBounds();
|
||||
double width = bounds.getWidth();
|
||||
double ascent = layout.getAscent();
|
||||
double descent = layout.getDescent();
|
||||
|
||||
AffineTransform oldTx = canvasGraphics.getTransform();
|
||||
AffineTransform flipTx = new AffineTransform();
|
||||
|
||||
if (mirrorDirection == MirrorDirection.HORIZONTAL) {
|
||||
double cx = x + width / 2.0;
|
||||
flipTx.translate(cx, 0);
|
||||
flipTx.scale(-1.0, 1.0);
|
||||
flipTx.translate(-cx, 0);
|
||||
} else if (mirrorDirection == MirrorDirection.VERTICAL) {
|
||||
double cy = y - (ascent - descent) / 2.0;
|
||||
flipTx.translate(0, cy);
|
||||
flipTx.scale(1.0, -1.0);
|
||||
flipTx.translate(0, -cy);
|
||||
}
|
||||
|
||||
canvasGraphics.transform(flipTx);
|
||||
try {
|
||||
canvasGraphics.drawString(iterator, x, y);
|
||||
} finally {
|
||||
canvasGraphics.setTransform(oldTx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -482,7 +610,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
@Override
|
||||
public void setComposite(Composite comp) {
|
||||
canvasGraphics.setComposite(comp);
|
||||
if (!drawOptions.contains(DrawOption.OUTLINE_MODE)) {
|
||||
canvasGraphics.setComposite(comp);
|
||||
}
|
||||
// FIXME: check this.
|
||||
}
|
||||
|
||||
@@ -758,7 +888,11 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
@Override
|
||||
public void setColor(Color c) {
|
||||
canvasGraphics.setColor(c);
|
||||
if (drawOptions.contains(DrawOption.MONOCHROMATIC) && c != null) {
|
||||
canvasGraphics.setColor(CalcUtils.convertToMonochrome(c));
|
||||
} else {
|
||||
canvasGraphics.setColor(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -24,13 +24,7 @@ package org.diylc.presenter;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
@@ -40,6 +34,7 @@ import org.diylc.clipboard.ComponentTransferable;
|
||||
import org.diylc.common.*;
|
||||
|
||||
import org.diylc.core.Angle;
|
||||
import org.diylc.core.ComponentGroup;
|
||||
import org.diylc.core.CreationMethod;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
@@ -68,9 +63,9 @@ public class InstantiationManager {
|
||||
public static final ComponentType clipboardType = new ComponentType("Clipboard contents",
|
||||
"Components from the clipboard", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
|
||||
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
|
||||
public static final ComponentType blockType = new ComponentType("Building block",
|
||||
"Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
|
||||
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
|
||||
// public static final ComponentType blockType = new ComponentType("Building block",
|
||||
// "Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
|
||||
// false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
|
||||
|
||||
public InstantiationManager() {}
|
||||
|
||||
@@ -128,23 +123,21 @@ public class InstantiationManager {
|
||||
firstControlPoint = scaledPoint;
|
||||
componentSlot = instantiateComponent(componentTypeSlot, firstControlPoint, currentProject);
|
||||
|
||||
// Set the other control point to the same location, we'll
|
||||
// move it later when mouse moves.
|
||||
componentSlot.get(0).setControlPoint(firstControlPoint, 0);
|
||||
componentSlot.get(0).setControlPoint(firstControlPoint, 1);
|
||||
// Collapse the component onto the first point, we'll stretch it later when mouse moves.
|
||||
componentSlot.get(0).stretchBetween(firstControlPoint, firstControlPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates component in the slot with the new second control point.
|
||||
*
|
||||
* Updates component in the slot with the new end control point.
|
||||
*
|
||||
* @param scaledPoint
|
||||
* @return true, if any change is made
|
||||
*/
|
||||
public boolean updatePointByPoint(Point2D scaledPoint) {
|
||||
boolean changeMade = !scaledPoint.equals(potentialControlPoint);
|
||||
potentialControlPoint = scaledPoint;
|
||||
if (componentSlot != null && !componentSlot.isEmpty()) {
|
||||
componentSlot.get(0).setControlPoint(scaledPoint, 1);
|
||||
if (componentSlot != null && !componentSlot.isEmpty() && firstControlPoint != null) {
|
||||
componentSlot.get(0).stretchBetween(firstControlPoint, scaledPoint);
|
||||
}
|
||||
return changeMade;
|
||||
}
|
||||
@@ -213,7 +206,10 @@ public class InstantiationManager {
|
||||
|
||||
// Update the component type slot so the app knows that something's
|
||||
// being instantiated.
|
||||
this.componentTypeSlot = autoGroup ? blockType : clipboardType;
|
||||
String slotName = Optional.ofNullable(componentTransferable.getName()).orElse("Building Block");
|
||||
this.componentTypeSlot = autoGroup ? new ComponentType(slotName,
|
||||
"Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
|
||||
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0) : clipboardType;
|
||||
|
||||
if (snapToGrid) {
|
||||
scaledPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY());
|
||||
@@ -222,9 +218,14 @@ public class InstantiationManager {
|
||||
// Update the location according to mouse location
|
||||
updateSingleClick(scaledPoint, snapToGrid, gridSpacing);
|
||||
|
||||
// copy group information if available
|
||||
// copy group information if available, but filter out invalid groups
|
||||
if (componentTransferable.getGroups() != null) {
|
||||
currentProject.getGroups().addAll(componentTransferable.getGroups());
|
||||
for (ComponentGroup group : componentTransferable.getGroups()) {
|
||||
if (group.getComponentIds() != null && !group.getComponentIds().isEmpty() &&
|
||||
group.getComponentIds().stream().anyMatch(id -> id != null)) {
|
||||
currentProject.getGroupsEx().add(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,11 +270,36 @@ public class InstantiationManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes an identifier to the front of the recently used list, removing any earlier occurrence
|
||||
* and trimming the list to {@link #MAX_RECENT_COMPONENTS}. If the identifier is already at the
|
||||
* front nothing is written. The identifier is either a fully qualified component class name or a
|
||||
* building block reference of the form {@code "block:<blockName>"} (see
|
||||
* {@link IBlockProcessor#BLOCK_PREFIX}).
|
||||
*
|
||||
* @param identifier component class name or {@code "block:<blockName>"}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void addToRecentComponents(String identifier) {
|
||||
List<String> recent = (List<String>) ConfigurationManager.getInstance()
|
||||
.readObject(IPlugInPort.RECENT_COMPONENTS_KEY, new ArrayList<String>());
|
||||
if (!recent.isEmpty() && recent.get(0).equals(identifier))
|
||||
return;
|
||||
// Remove if it's already somewhere in the list.
|
||||
recent.remove(identifier);
|
||||
// Add to the front of the list.
|
||||
recent.add(0, identifier);
|
||||
// Trim the list if necessary.
|
||||
while (recent.size() > MAX_RECENT_COMPONENTS)
|
||||
recent.remove(recent.size() - 1);
|
||||
ConfigurationManager.getInstance().writeValue(IPlugInPort.RECENT_COMPONENTS_KEY, recent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<IDIYComponent<?>> instantiateComponent(ComponentType componentType, Point2D point,
|
||||
Project currentProject) throws InstantiationException, IllegalAccessException, NoSuchMethodException,
|
||||
SecurityException, IllegalArgumentException, InvocationTargetException {
|
||||
LOG.info("Instatiating component of type: " + componentType.getInstanceClass().getName());
|
||||
LOG.info("Instantiating component of type: " + componentType.getInstanceClass().getName());
|
||||
|
||||
// Instantiate the component.
|
||||
IDIYComponent<?> component;
|
||||
@@ -308,21 +334,7 @@ public class InstantiationManager {
|
||||
}
|
||||
|
||||
// Write to recent components
|
||||
List<String> recentComponentTypes =
|
||||
(List<String>) ConfigurationManager.getInstance().readObject(IPlugInPort.RECENT_COMPONENTS_KEY,
|
||||
new ArrayList<ComponentType>());
|
||||
String className = componentType.getInstanceClass().getName();
|
||||
if (recentComponentTypes.size() == 0 || !recentComponentTypes.get(0).equals(className)) {
|
||||
// Remove if it's already somewhere in the list.
|
||||
recentComponentTypes.remove(className);
|
||||
// Add to the end of the list.
|
||||
recentComponentTypes.add(0, className);
|
||||
// Trim the list if necessary.
|
||||
if (recentComponentTypes.size() > MAX_RECENT_COMPONENTS) {
|
||||
recentComponentTypes.remove(recentComponentTypes.size() - 1);
|
||||
}
|
||||
ConfigurationManager.getInstance().writeValue(IPlugInPort.RECENT_COMPONENTS_KEY, recentComponentTypes);
|
||||
}
|
||||
addToRecentComponents(componentType.getInstanceClass().getName());
|
||||
|
||||
List<IDIYComponent<?>> list = new ArrayList<IDIYComponent<?>>();
|
||||
list.add(component);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
You are helping me generate Appendix A ? Component Model Reference for an ebook about DIY Layout Creator (DIYLC).
|
||||
|
||||
I will provide you with a JSON file that defines all component types used by DIYLC.
|
||||
|
||||
Your task
|
||||
|
||||
Convert the JSON into a single DOCX appendix that documents all components defined in the JSON, with no omissions and no inferred or invented information.
|
||||
|
||||
Structure rules (must follow exactly)
|
||||
1. Document structure
|
||||
|
||||
Output format: DOCX
|
||||
|
||||
Document title (H1):
|
||||
Appendix A ? Component Model Reference
|
||||
|
||||
Group components by category
|
||||
|
||||
Each category is an H2 heading
|
||||
|
||||
Category order: alphabetical
|
||||
|
||||
Component order within category: alphabetical
|
||||
|
||||
2. Component entry format
|
||||
|
||||
Do NOT use H3 or any heading for component names.
|
||||
|
||||
For each component, render the following:
|
||||
|
||||
Component Name (bold, normal paragraph)
|
||||
Description: <exact description text from JSON>
|
||||
Creation method: <human-friendly label>
|
||||
|
||||
|
||||
Creation method mapping:
|
||||
|
||||
POINT_BY_POINT ? Point-by-point
|
||||
|
||||
SINGLE_CLICK ? Single-click
|
||||
|
||||
Missing value ? (not specified)
|
||||
|
||||
3. Properties table
|
||||
|
||||
If the component defines properties, render a table immediately after the metadata:
|
||||
|
||||
| Property | Type | Possible values |
|
||||
|
||||
Table formatting requirements:
|
||||
|
||||
No visible borders (markdown-style look)
|
||||
|
||||
Column widths:
|
||||
|
||||
Property: 30%
|
||||
|
||||
Type: 30%
|
||||
|
||||
Possible values: 40%
|
||||
|
||||
Header row:
|
||||
|
||||
Bold text
|
||||
|
||||
Very light gray background (#EEEEEE)
|
||||
|
||||
Data rows:
|
||||
|
||||
Alternating row background
|
||||
|
||||
Very very light gray (#F7F7F7) on every second row
|
||||
|
||||
Row height:
|
||||
|
||||
As compact as possible
|
||||
|
||||
No extra vertical padding
|
||||
|
||||
Content rules:
|
||||
|
||||
Property name ? JSON name
|
||||
|
||||
Type ? JSON type
|
||||
|
||||
Possible values:
|
||||
|
||||
Populate only if the JSON defines an enum/list
|
||||
|
||||
Leave empty otherwise
|
||||
|
||||
If a component has no properties, write:
|
||||
|
||||
This component has no configurable properties.
|
||||
|
||||
4. Styling consistency
|
||||
|
||||
Font: Calibri
|
||||
|
||||
Base text size: 11 pt
|
||||
|
||||
Match the visual style of previously generated ebook chapters
|
||||
|
||||
No decorative elements
|
||||
|
||||
Clean, technical, reference-style layout
|
||||
|
||||
Strict constraints
|
||||
|
||||
? Do NOT summarize components
|
||||
|
||||
? Do NOT invent descriptions, properties, or values
|
||||
|
||||
? Do NOT rename properties or types
|
||||
|
||||
? Do NOT add commentary or interpretation
|
||||
|
||||
? Do NOT introduce additional columns
|
||||
|
||||
Everything must come directly from the JSON.
|
||||
|
||||
Final output
|
||||
|
||||
Produce one DOCX file
|
||||
|
||||
File name:
|
||||
Appendix A ? Component Model Reference.docx
|
||||
|
||||
When finished, provide a download link to the DOCX file.
|
||||
|
||||
Acknowledge when you are ready to receive the JSON file.
|
||||
@@ -32,6 +32,8 @@ import com.thoughtworks.xstream.converters.UnmarshallingContext;
|
||||
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
|
||||
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
|
||||
|
||||
import org.diylc.utils.MathUtils;
|
||||
|
||||
/**
|
||||
* Serializes {@link Area} objects by converting coordinates from pixels to inches, thus avoiding
|
||||
* the resolution to affect point placement.
|
||||
@@ -66,7 +68,7 @@ public class AreaConverter implements Converter {
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
writer.startNode("coordinate");
|
||||
writer.setValue(String.valueOf(coord[i]));
|
||||
writer.setValue(MathUtils.formatDouble(coord[i]));
|
||||
writer.endNode();
|
||||
}
|
||||
writer.endNode();
|
||||
@@ -87,7 +89,7 @@ public class AreaConverter implements Converter {
|
||||
List<java.lang.Double> values = new ArrayList<java.lang.Double>();
|
||||
while (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
values.add(java.lang.Double.parseDouble(reader.getValue()));
|
||||
values.add(MathUtils.parseDouble(reader.getValue()));
|
||||
reader.moveUp();
|
||||
}
|
||||
java.lang.Double[] coord = values.toArray(new java.lang.Double[0]);
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader;
|
||||
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
|
||||
|
||||
import org.diylc.utils.Constants;
|
||||
import org.diylc.utils.MathUtils;
|
||||
|
||||
/**
|
||||
* Serializes {@link Point2D} objects by converting coordinates from pixels to inches, thus avoiding
|
||||
@@ -42,26 +43,27 @@ public class PointConverter implements Converter {
|
||||
@Override
|
||||
public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingContext context) {
|
||||
Point2D point = (Point2D) object;
|
||||
writer.addAttribute("x", Double.toString(1d * point.getX() / Constants.PIXELS_PER_INCH));
|
||||
writer.addAttribute("y", Double.toString(1d * point.getY() / Constants.PIXELS_PER_INCH));
|
||||
writer.addAttribute("x", MathUtils.formatDouble(1d * point.getX() / Constants.PIXELS_PER_INCH));
|
||||
writer.addAttribute("y", MathUtils.formatDouble(1d * point.getY() / Constants.PIXELS_PER_INCH));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
|
||||
double x;
|
||||
double y;
|
||||
if (reader.hasMoreChildren()) {
|
||||
reader.moveDown();
|
||||
x = Double.parseDouble(reader.getValue());
|
||||
x = MathUtils.parseDouble(reader.getValue());
|
||||
reader.moveUp();
|
||||
reader.moveDown();
|
||||
y = Double.parseDouble(reader.getValue());
|
||||
y = MathUtils.parseDouble(reader.getValue());
|
||||
reader.moveUp();
|
||||
return new Point2D.Double(x, y);
|
||||
} else {
|
||||
x = Double.parseDouble(reader.getAttribute("x"));
|
||||
y = Double.parseDouble(reader.getAttribute("y"));
|
||||
return new Point2D.Double(x * Constants.PIXELS_PER_INCH, y * Constants.PIXELS_PER_INCH);
|
||||
x = MathUtils.round(Double.parseDouble(reader.getAttribute("x")) * Constants.PIXELS_PER_INCH);
|
||||
y = MathUtils.round(Double.parseDouble(reader.getAttribute("y")) * Constants.PIXELS_PER_INCH);
|
||||
return new Point2D.Double(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
@@ -54,6 +55,7 @@ import com.thoughtworks.xstream.mapper.MapperWrapper;
|
||||
import com.thoughtworks.xstream.security.AnyTypePermission;
|
||||
|
||||
import org.diylc.common.EventType;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.presenter.Presenter;
|
||||
import org.diylc.test.DIYTest;
|
||||
@@ -112,6 +114,7 @@ public class ProjectFileManager {
|
||||
xStream.autodetectAnnotations(true);
|
||||
xStream.alias("point", java.awt.geom.Point2D.class);
|
||||
xStream.alias("point", java.awt.geom.Point2D.Double.class);
|
||||
xStream.alias("group", org.diylc.core.ComponentGroup.class);
|
||||
xStream.alias("font", java.awt.Font.class);
|
||||
xStream.alias("project", Project.class);
|
||||
xStream.aliasPackage("diylc", "org.diylc.components");
|
||||
@@ -278,9 +281,29 @@ public class ProjectFileManager {
|
||||
LOG.warn("The opened file is missing the following properties: " + String.join(", ", missingFields));
|
||||
warnings.add("The project references unknown component properties, most likely because it was created with a newer version of DIYLC.");
|
||||
}
|
||||
ensureUniqueComponentIds(project);
|
||||
fis.close();
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures no two components share the same identifier. Assigns a new UUID to any
|
||||
* component that has a null id or an id already seen (duplicate).
|
||||
*/
|
||||
private void ensureUniqueComponentIds(Project project) {
|
||||
if (project == null || project.getComponents() == null) {
|
||||
return;
|
||||
}
|
||||
Set<UUID> seenIds = new HashSet<UUID>();
|
||||
for (IDIYComponent<?> component : project.getComponents()) {
|
||||
UUID id = component.getId();
|
||||
if (id == null || seenIds.contains(id)) {
|
||||
component.setId(UUID.randomUUID());
|
||||
LOG.warn("Detected duplicated componentId: " + id);
|
||||
}
|
||||
seenIds.add(component.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private VersionNumber readV3Version(String fileName) throws Exception {
|
||||
File fXmlFile = new File(fileName);
|
||||
|
||||
@@ -81,6 +81,7 @@ public class BomMaker {
|
||||
(Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
if (type.getBomPolicy() == BomPolicy.NEVER_SHOW)
|
||||
continue;
|
||||
String typeName = ComponentProcessor.getDisplayTypeName(component, type);
|
||||
String name = component.getName();
|
||||
String value;
|
||||
try {
|
||||
@@ -89,7 +90,7 @@ public class BomMaker {
|
||||
value = "<undefined>";
|
||||
}
|
||||
if ((name != null) && (value != null)) {
|
||||
String key = type.getName() + "|" + value;
|
||||
String key = typeName + "|" + value;
|
||||
if (entryMap.containsKey(key)) {
|
||||
BomEntry entry = entryMap.get(key);
|
||||
entry.setQuantity(entry.getQuantity() + 1);
|
||||
@@ -98,7 +99,7 @@ public class BomMaker {
|
||||
}
|
||||
} else {
|
||||
entryMap.put(key,
|
||||
new BomEntry(type.getName(), type.getBomPolicy() == BomPolicy.SHOW_ALL_NAMES ? name : type.getName(),
|
||||
new BomEntry(typeName, type.getBomPolicy() == BomPolicy.SHOW_ALL_NAMES ? name : typeName,
|
||||
value, 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.utils;
|
||||
|
||||
public class MathUtils {
|
||||
|
||||
public static String formatDouble(double value) {
|
||||
return Double.toString(Math.round(value * 1e8) / 1e8);
|
||||
}
|
||||
|
||||
public static Double round(double value) {
|
||||
return Math.round(value * 1e8) / 1e8;
|
||||
}
|
||||
|
||||
public static double parseDouble(String str) {
|
||||
return Math.round(Double.parseDouble(str) * 1e8) / 1e8;
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ public class VersionReader {
|
||||
sbHtml.append(" </release>\n");
|
||||
|
||||
System.out.println(sbMd.toString());
|
||||
System.out.println();
|
||||
System.out.println(sbHtml.toString());
|
||||
// System.out.println();
|
||||
// System.out.println(sbHtml.toString());
|
||||
in.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,20 +2,32 @@
|
||||
<a class="org.diylc.appframework.update.Version-array">
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>4</major>
|
||||
<minor>41</minor>
|
||||
<build>1</build>
|
||||
<major>5</major>
|
||||
<minor>10</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-03-08 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-01-20 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Do not mirror bottom layers for Gerber export as fabrication service providers expect a top view for everything</description>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'M' and 'N' potentiometer taper options (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added E-C-B BJT pinout to TO-92 transistor package</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Selecting components by drawing a rectangle around them works glitchy</description>
|
||||
<description>Fixed the performance issue that happens when groups of components are copy-pasted and the project size grows uncontrollably</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>All building blocks of the same type get added to the same group</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Tactile Micro Switch' editor doesn't show up on double click</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
@@ -23,415 +35,543 @@
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>11</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2026-02-15 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to export chat history from 'AI Assistant' to a HTML file</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Upgraded AI models for AI-powered tools and improved overall quality of the responses</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Windows installer prompts for installation folder instead of forcing one</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Shape components shouldn't appear in the BOM</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved rendering of 'Switch (Latching)' symbol to clearly make visual contact between the terminals</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Consolidate a few keyboard shortcuts for consistency</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Using "Duplicate Selection" results in duplicates of component ids, breaking Project Explorer panel and causing other issues downstream</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Calculated length disappeared from Hookup Wire and similar curved components</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Resize dimensions do not show for 'Chassis Panel' component when dragging a corner point</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>12</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2026-03-26 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Auto-Unit Conversion' configuration which automatically converts between metric and imperial units based on the ruler unit preference</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added a new section in the output of 'AI Circuit Analyzer' which provides insights into the operation of the circuit and individual components</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Better error handling for AI tools</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Pressing the 'Esc' key closes selection editor dialog</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Right click (or two finger tap on mac) after selecting a component with a regular click opens editor dialog in addition to the context menu</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Clicking on a component type in the left-hand component tree sometimes does not register and does not instantiate the component</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>13</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2026-04-07 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added new 'Heat Sink Resistor' component with a bunch of factory variants covering the most common types</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added new 'Dial Scale' variations - 'Ticks + Numeric (0/5.5/11)' a'la Friedman and another variation of 'Ticks' with more pronounced 9/12/3 o'clock indices</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Core' property to 'Radial Inductor' component that allows selection between Ferrite and Air core inductor models'</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Envelope' property to tube symbol components which can be used to display one side of the outer envelope dashed indicating section A or section B of a tube</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added more ceramic resistor variants to 'Resistor' component</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Uniformized rendering of components while being dragged</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Ability to blank-out previously populated values for unit-type measures, e.g. resistance, capacitance, dimensions, etc</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved data model for storing transparency alpha and other properties that are edited using a slider widget; as a result, the editing is more precise and does not randomly change to neighboring values</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved editing experience of integer properties (e.g. number of terminals on a tag strip) by using the spinner text field</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved logic when moving component(s) that have other components stuck with them; in case there are non-sticky control points within the included components, they will be moved along retaining component shape (e.g. wires or curved traces)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Decommission 'Loadline Editor' option from the File menu</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>14</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2026-06-03 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Potentiometer (Miniature)' component with two variations - right-angled and vertically mounted</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Thonk 3.5mm Jack' component with two variations - mono switched and stereo</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Enable all types of potentiometers and jacks for guitar diagram analysis</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increase maximum number of switch combination for analysis to 1024</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>'Line' component set to px used to render slightly thicker than 1px, appearing thicker than schematic symbols; changed to match the thickenss of schematic symbols</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>15</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2026-06-13 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Add Freeway Blade switches 3B3-01, 5B5-01, 5B5-02 (thank you Martin Morell)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>IEC variations of fuse and potentiometer symbols</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Detect the style of a treble bleed based on the topology and component values (e.g. Fender, DiMarzio, Kinman, etc)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved quality of AI Assistant responses in some cases</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved precision of rendering, in some cases there was a sub-pixel misalignment between the lines that should meet exactly</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Tone capacitor mistaken for treble bleed when connected to the input lug of the volume pot</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Dragging a component from the component tree to the canvas sometimes does not work as expected without clicking on it first and then dragging it</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Schaller Megaswitch' not rendered correctly when rotated for 90 or 270 degrees</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>6</major>
|
||||
<minor>0</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-03-09 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-06-13 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Switched to AppImage distribution for Linux system, abandoning Flatpak as a means of distribution. DIYLC AppImage will be available alongside with other executables on the official DIYLC GitHub page.</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Upgraded Windows installer version and Windows EXE launcher version</description>
|
||||
<description>Upgrade Java version to 21</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed alignment issues of round elements (e.g. solder pads and holes) in exported Gerber files</description>
|
||||
<description>Fixed scaling for 'Image' and 'SVG Image' components; it was showing as unrecognized</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed the issue with opening DIY files on Mac OS</description>
|
||||
<description>Fixed errors while analyzing guitar wiring diagrams caused by treble bleed enhancements</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<major>6</major>
|
||||
<minor>1</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-03-24 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-06-30 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to render the underside of boards along with the top side for easier validation, building and debugging</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Configurable 'High DPI Rendering' feature which makes the diagram smoother on high-resolution displays but can affect performance</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Significantly improved rendering performance</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increased initial memory allocation to 1GB for smoother performance</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Upgraded source code structure to modern standards and introduced Maven</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved robustness of series/parallel graph construction code used in circuit analysis</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Omit boards, wires, traces, terminals, etc from Spice netlist output</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Updated the windows installer (hopefully) to avoid false positive anti-virus detection</description>
|
||||
<description>Improved handling of larger AI analysis requests and added more token budget per request to Supporter and Pro plans</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Moving selected components with a mouse doesn't register file modification action and doesn't enable Undo action</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Rotating selected components sometimes wacks the alignment off the grid</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Ctrl+X (Command+X) shortcut for Cut action doesn't work because 'Project Explorer' steals the focus on X key press</description>
|
||||
<description>Files containing Inductor Symbol fail to open</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>1</minor>
|
||||
<build>1</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-03-27 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Include windows ZIP package</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Windows version doesn't start in some cases</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<major>6</major>
|
||||
<minor>2</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-04-12 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-08-06 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'AI Assistant' that can help with general electronics questions, questions about DIYLC usage and circuit-related questions</description>
|
||||
<description>Added two new view perspectives to Potentiometer component - Terminals Up and Terminals down</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>'Virtual Multimeter' from 'Analyze' menu traces the circuit finding all possible paths of electricity between two points designated by the probe symbol</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to display switch lug markings in order to illustrate how it works internally</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added a 'Save' button to all analysis screens which saves results of a circuit analysis to an HTML file</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability control if the board underside display should render as transparent or not</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added DIY Cloud password reset page</description>
|
||||
<description>Use either username of email to log into DIYLC cloud</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Moved 'Analyze' functions from 'File' menu to their own menu for faster access</description>
|
||||
<description>Improved comparison logic of the 'Compare With..' tool</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Moved 'Highlight Connected Areas' feature from 'Config' menu to 'Analyze' menu</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Fully configurable colors for 'Mini Toggle Switch' components</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Removed 'DP3T' options from 'Mini Toggle Switch' because that is covered by 'Slide Switch' component and does not belong here</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Include components in the underside display that have at least one control point laying in the board, as opposed to all</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Applying a variant to a 'Hookup Wire' or 'Curved Trace' makes it lose its shape</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Copy operation doesn't work when certain component types are selected</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Board underside display does not work if there's a Label or PCB Text component on the board</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Using board underside display sometimes makes other components semi-transparent</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed Windows ZIP package issue with the bundled JRE</description>
|
||||
<description>Align Potentiometer 1-2-3 markings with circuit analysis node labels; it used to be mixed up in some cases</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<major>6</major>
|
||||
<minor>3</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-04-21 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-08-22 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved AI Assistant quality of responses, especially when analyzing a circuit</description>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added support for stacked guitar pickups: 'Stacked - North' and 'Stacked - South' are two new options to pick in the Coil(s) property of Single Coil, P-90, P-Bass and Jazz Bass pickups</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Node Name Tooltips' feature that can be toggled in the Config menu; when enabled a tooltip is shown next to the cursor with names of component nodes under the cursor</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>When saving a project to the new file, retain all the AI Assistant chat history for the new project file</description>
|
||||
<description>Significantly improved AI Assistant's knowledge base on DIYLC usage by ingesting most of the material from the eBook</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Added ability to be logged into the same DIYLC Cloud account from several machines at the same time</description>
|
||||
<description>Improved the accuracy of board underside rendering; some of the components were not rendered in the right place or at all</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Confirmation dialog before deleting AI Assistant chat history</description>
|
||||
<description>Show a progress dialog while waiting for a long-running AI Analysis to complete</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Output current subscription statistics at the end of the chat for usage transparency</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved styling and usability of chatbot tool</description>
|
||||
<description>Renamed Mac OS build files to start with a 'mac' prefix instead of 'osx'</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Language selection doesn't work on some OSs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Connect to Patreon' option doesn't work if the user name contains a space or another non-alphanumerical character</description>
|
||||
<description>Images can randomly disappear when zooming in and scrolling</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<major>6</major>
|
||||
<minor>4</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-05-13 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-09-04 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>'AI Analyzer' feature added which uses AI to scan the circuit diagram and report issues and suggestions</description>
|
||||
<description>Building blocks instantiate by default as a single component, inheriting control points and terminal nodes from all individual components but acting as a single component; still possible to instantiate individual components using 'Insert as Grouped Components' action from the context menu over a building block</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to set node name (or names) for some connectivity components (e.g. Solder Pad, Eyelet, PCB Terminal Block, etc); when combined into building blocks, these names will become node names of the composite component stamped from the building block</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Significantly improve rendering performance of most components</description>
|
||||
<description>Include building blocks in the Recently Used category</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>'Open Jack 1/4"' and 'Potentiometer Panel' now tolerate connections anywhere on the lugs instead of dead center only</description>
|
||||
<description>When saving building blocks, present a drop down list with most recently used building blocks for quick selection</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increased maximum number of switching combinations to 256 for circuit analysis</description>
|
||||
<description>Prompt to confirm the overwrite when saving over the existing building blocks</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Pilot Lamp Holder' component can sometimes hang the application</description>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>When transistor pinout is selected for TO-1, TO-92, TO-126, TO-220 transistor, the corresponding electrode name will be used as a node name in programmatical and AI analysis</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed the issue with embedding image files into DIY projects</description>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Diode components now label their terminal nodes as 'A' (anode) and 'K' (cathode) instead of '1' and '2'</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed the issue with some components (e.g. DIP IC) jumping when showing underside of a board</description>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Capacitor components now label their terminal nodes as '+' and '-' instead of '1' and '2' when 'Polarized' property is ticked</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Percentage values such as transparency Alpha, image Scale, etc, can have decimal values and not just integers</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Made announcement bubbles and dialogs that contain links clickable</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<major>6</major>
|
||||
<minor>5</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-05-26 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-09-11 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Compare With...' tool in the 'Analyze' menu that compares the content of two DIY files electronically and reports differences. Can be useful to compare two versions of the same circuit or validate a layout against the schematics file.</description>
|
||||
<description>Arduino Mega 2560</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Arduino Nano - ATmega328, 33 BLE, 33 BLE Sense, 33 IoT, ESP32, Every, R4, RP3040 Connect</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Arduino Uno - R3, R4 Minima, R4 WiFi, Leonardo</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>ESP32 - DevKit V1 (30-pin), DevKitC V4 (38-pin), S3 DevKitC-1 (44-pin), C3 DevKitM-1 (30-pin), C6 DevKitC-1 (32-pin)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>ESP8266 NodeMCU</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Raspberry Pi - 3B+, 4B, 5</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Raspberry Pi - Pico, Pico W, Pico 2, Pico 2 W</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Raspberry Pi - Zero, Zero 2, Zero 2 W</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Teensy - 3.2, 4.0, 4.1</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>WeMos D1 Mini</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increased tolerance of sticky points on guitar pickup terminals in order to make it easier for leads to make proper contact and for the circuit analyzer to work</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved trace proximity calculation to avoid showing false positives</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved the styling of circuit analysis reports</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>X or Q keys sometimes do not register in secondary dialogs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Auto-Edit Mode' doesn't do what it's supposed to</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>If the internet connection is present, but unstable, the application starts but nothing can be clicked until the connection restores</description>
|
||||
<description>Upgraded AI models to faster and smarter versions across all tiers, especially Supporter and Pro</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<major>6</major>
|
||||
<minor>6</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-08-11 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-09-29 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Drill Hole' component that can be used for board mounting holes, chassis holes, faceplace holes, etc</description>
|
||||
<description>Added 'Right Angle Wire' and 'Right Angle Line' components that route between two points using only horizontal and vertical runs with rounded corners</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Double click on a component type in the tree or the toolbar to instantiate it at once, even for those that are intended to be placed point-by-point</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Simplify zoom in/out keyboard shortcuts to only plus/minus keys and zero numerical key for zoom reset to 100</description>
|
||||
<description>All curved components ('Curved Trace', 'Hookup Wire', 'Twisted Leads') are now getting created between the two points by default instead of getting created with some preset shape and length</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Bundle Java JRE with Linux AppImage to avoid having to install it</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Updated naming conventions Mac build packages to replace 'arm' with 'silicon' and 'x86' to 'intel' for clarity</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Allow Windows 64 bit installer to run on ARM-based Windows machine (through emulation)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>More intuitive user account creation dialog for DIYLC Cloud - showing exact requirements for password and marking optional entries</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved project search algorithm in DIYLC cloud</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>When a switch position is selected (i.e. set to a value other than None), DIYLC will treat the terminals that are internally connected in that switching logic as connected in analysis tools</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increase maximum size of the backup folder from 64MB to 1GB for deeper history</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Alignment issues of some visual elements in the resulting Gerber files, e.g. DIL IC pin designators</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Connect to Patreon' option says that it connected account with username 'a b c' but doesn't do anything</description>
|
||||
<description>Improve terminal node naming within building blocks to ensure no duplicates by adding a name of the nested component as a prefix</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<major>6</major>
|
||||
<minor>7</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-12-21 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2026-10-09 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added '6P5T (5-Position 6 Pole AxLabs)' lever switch type (thank you mm-rnd)</description>
|
||||
<description>7-Segment Display - 1-digit 0.56 inch, 2-digit 0.56 inch, 4-digit in 0.36 inch and 0.56 inch, and TM1637 driver module</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added '4P3T (3-Position 4 Pole AxLabs)' lever switch type (thank you mm-rnd)</description>
|
||||
<description>Character LCD - 16x2 and 20x4, with I2C backpack or 16-pin parallel interface</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'DP3T (Import 5-Position 2502N)' lever switch type (thank you mm-rnd)</description>
|
||||
<description>LED Bar Graph - 4, 5, 6, 8, 10 and 12 segments</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>LED Matrix (MAX7219) - single 8x8, compact 8x8, 4-in-1 32x8, 8-in-1 64x8</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>NeoPixel Breakout (WS2812B) - single LED</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>NeoPixel Jewel (WS2812B) - 7 LEDs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>NeoPixel Panel (WS2812B) - 8x8, 64 LEDs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>NeoPixel Ring (WS2812B) - 12, 16 and 24 LEDs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>NeoPixel Stick (WS2812B) - 8 LEDs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>NeoPixel Strip (WS2812B) - 30, 60 and 144 LEDs per meter, up to 144 LEDs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Nokia 5110 LCD (PCD8544)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>OLED Display - 0.96 inch SSD1306, 0.91 inch SSD1306, 1.3 inch SH1106, in I2C, 7-pin SPI and 6-pin SPI</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>TFT Display - 2.8 inch and 2.4 inch ILI9341, 1.8 inch and 0.96 inch ST7735, 1.54 inch ST7789, 1.28 inch round GC9A01</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to hide component categories in the component tree and toolbar from the Config menu</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Corrected 'S1 Switch' up/down position designators (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Corrected 'Mini Toggle Switch' position names for center-OFF variants to be in the correct order (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>'Drill Hole' changes color to red when selected</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>8</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-12-21 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved sensitivity of 'Multimeter Probe' that required pixel-level precision to detect connectivity; improved rendering of the probe in outline mode</description>
|
||||
<description>Bill of Materials now lists the selected variant of microcontroller boards and displays, so different versions of the same board are no longer merged into a single row</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed underside rendering and mirroring position issues that affected only SPDT 'Mini Toggle Switch' types</description>
|
||||
<description>4-pin Jumbo Tube Socket - the two filament pins are now labeled F1 and F2 instead of both being labeled F, so they no longer read as the same node</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Netlists now treat the repeated supply pins of a microcontroller or display board as one node, so grounding a single GND pin no longer leaves the others floating</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
|
||||
@@ -4773,5 +4773,557 @@
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>4</major>
|
||||
<minor>41</minor>
|
||||
<build>1</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-03-08 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Do not mirror bottom layers for Gerber export as fabrication service providers expect a top view for everything</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Selecting components by drawing a rectangle around them works glitchy</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>0</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-03-09 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Switched to AppImage distribution for Linux system, abandoning Flatpak as a means of distribution. DIYLC AppImage will be available alongside with other executables on the official DIYLC GitHub page.</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Upgraded Windows installer version and Windows EXE launcher version</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed alignment issues of round elements (e.g. solder pads and holes) in exported Gerber files</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed the issue with opening DIY files on Mac OS</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>1</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-03-24 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to render the underside of boards along with the top side for easier validation, building and debugging</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Configurable 'High DPI Rendering' feature which makes the diagram smoother on high-resolution displays but can affect performance</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Significantly improved rendering performance</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increased initial memory allocation to 1GB for smoother performance</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Upgraded source code structure to modern standards and introduced Maven</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved robustness of series/parallel graph construction code used in circuit analysis</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Omit boards, wires, traces, terminals, etc from Spice netlist output</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Updated the windows installer (hopefully) to avoid false positive anti-virus detection</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Moving selected components with a mouse doesn't register file modification action and doesn't enable Undo action</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Rotating selected components sometimes wacks the alignment off the grid</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Ctrl+X (Command+X) shortcut for Cut action doesn't work because 'Project Explorer' steals the focus on X key press</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>1</minor>
|
||||
<build>1</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-03-27 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Include windows ZIP package</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Windows version doesn't start in some cases</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>2</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-04-12 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'AI Assistant' that can help with general electronics questions, questions about DIYLC usage and circuit-related questions</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>'Virtual Multimeter' from 'Analyze' menu traces the circuit finding all possible paths of electricity between two points designated by the probe symbol</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to display switch lug markings in order to illustrate how it works internally</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added a 'Save' button to all analysis screens which saves results of a circuit analysis to an HTML file</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability control if the board underside display should render as transparent or not</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added DIY Cloud password reset page</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Moved 'Analyze' functions from 'File' menu to their own menu for faster access</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Moved 'Highlight Connected Areas' feature from 'Config' menu to 'Analyze' menu</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Fully configurable colors for 'Mini Toggle Switch' components</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Removed 'DP3T' options from 'Mini Toggle Switch' because that is covered by 'Slide Switch' component and does not belong here</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Include components in the underside display that have at least one control point laying in the board, as opposed to all</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Applying a variant to a 'Hookup Wire' or 'Curved Trace' makes it lose its shape</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Copy operation doesn't work when certain component types are selected</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Board underside display does not work if there's a Label or PCB Text component on the board</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Using board underside display sometimes makes other components semi-transparent</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed Windows ZIP package issue with the bundled JRE</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>3</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-04-21 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved AI Assistant quality of responses, especially when analyzing a circuit</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>When saving a project to the new file, retain all the AI Assistant chat history for the new project file</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Added ability to be logged into the same DIYLC Cloud account from several machines at the same time</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Confirmation dialog before deleting AI Assistant chat history</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Output current subscription statistics at the end of the chat for usage transparency</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved styling and usability of chatbot tool</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Language selection doesn't work on some OSs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Connect to Patreon' option doesn't work if the user name contains a space or another non-alphanumerical character</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>4</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-05-13 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>'AI Analyzer' feature added which uses AI to scan the circuit diagram and report issues and suggestions</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Significantly improve rendering performance of most components</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>'Open Jack 1/4"' and 'Potentiometer Panel' now tolerate connections anywhere on the lugs instead of dead center only</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increased maximum number of switching combinations to 256 for circuit analysis</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Pilot Lamp Holder' component can sometimes hang the application</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed the issue with embedding image files into DIY projects</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed the issue with some components (e.g. DIP IC) jumping when showing underside of a board</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>5</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-05-26 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Compare With...' tool in the 'Analyze' menu that compares the content of two DIY files electronically and reports differences. Can be useful to compare two versions of the same circuit or validate a layout against the schematics file.</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increased tolerance of sticky points on guitar pickup terminals in order to make it easier for leads to make proper contact and for the circuit analyzer to work</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved trace proximity calculation to avoid showing false positives</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved the styling of circuit analysis reports</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>X or Q keys sometimes do not register in secondary dialogs</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Auto-Edit Mode' doesn't do what it's supposed to</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>If the internet connection is present, but unstable, the application starts but nothing can be clicked until the connection restores</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>6</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-08-11 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Drill Hole' component that can be used for board mounting holes, chassis holes, faceplace holes, etc</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Simplify zoom in/out keyboard shortcuts to only plus/minus keys and zero numerical key for zoom reset to 100</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Bundle Java JRE with Linux AppImage to avoid having to install it</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Updated naming conventions Mac build packages to replace 'arm' with 'silicon' and 'x86' to 'intel' for clarity</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Allow Windows 64 bit installer to run on ARM-based Windows machine (through emulation)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>More intuitive user account creation dialog for DIYLC Cloud - showing exact requirements for password and marking optional entries</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved project search algorithm in DIYLC cloud</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>When a switch position is selected (i.e. set to a value other than None), DIYLC will treat the terminals that are internally connected in that switching logic as connected in analysis tools</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Increase maximum size of the backup folder from 64MB to 1GB for deeper history</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Alignment issues of some visual elements in the resulting Gerber files, e.g. DIL IC pin designators</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Connect to Patreon' option says that it connected account with username 'a b c' but doesn't do anything</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>7</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-12-21 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added '6P5T (5-Position 6 Pole AxLabs)' lever switch type (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added '4P3T (3-Position 4 Pole AxLabs)' lever switch type (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'DP3T (Import 5-Position 2502N)' lever switch type (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Corrected 'S1 Switch' up/down position designators (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Corrected 'Mini Toggle Switch' position names for center-OFF variants to be in the correct order (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>'Drill Hole' changes color to red when selected</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>8</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2025-12-30 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added Silkscreen export to PDF/PNG and print capabilities</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Multiwatt IC' package with 11 and 15 pin varieties</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added new property in 'LP Switch' that allows having the middle terminals connected (as they were) or disconnected to allow more flexibility</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Ending Style' property to 'Copper Trace' and 'Curved Trace' to allow choosing round or flat style</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added Chinese simplified (thank you chenjingyuanku) and Hindi UI translation and updated all other languages with new translations</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added configuration for text scale factor in rare cases when system scale factor is not applied correctly; requires adding a configuration property to config.xml with key 'fontScaleFactor' and a desired float value</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved behavior of mouse cursor during drag operations; this should also fix behavior on some linux machines that show a "forbidden" cursor while dragging</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improved sensitivity of 'Multimeter Probe' that required pixel-level precision to detect connectivity; improved rendering of the probe in outline mode</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Improve contrast of tooltips on some Linux machines that show white text against yellow balloon</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Honor label color settings for 'Tape Measure' component</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Reorganized export and print related items in the File menu to reduce clutter</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Fixed underside rendering and mirroring position issues that affected only SPDT 'Mini Toggle Switch' types</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Application freezes when using underside rendering or mirroring on 'Pin Header' when sized at 1x1</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>'Rotary Switch (Open)' cannot be selected by clicking on the wafer for 4-pole variations</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>5</major>
|
||||
<minor>9</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2026-01-10 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added ability to name groups of components</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Show groups of components as single items in the Project Explorer list</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added balloon tooltip next to cursor showing component width, height and length (where available) while resizing; can be disabled from Config menu</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Freeway 3x3-05 (Ultra) Switch' (thank you mm-rnd)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Configurable 'Tactile Micro Switch' dimensions and added a few most commonly used variants</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Exposed the most common pin pairing modes and added switching logic for 'Tactile Micro Switch' to allow using it for analysis</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>'Potentiometer Panel' can now render numerical lug markers for easier identification</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>'Auto-Wrap Label' retains line breaks from the input text</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>'Cliff 1/8" Jack', 'Open 1/8" Jack' and 'Open 1/4" Jack' can be rotated</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Fixed the offset of terminals on a DP4T Tele type of 'Lever Switch' to be aligned with the physical switch (attention - this will break existing diagrams and will require adjusting leads to match the new terminal positions)</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Make the label of schematic symbols (e.g. 'Resistor Symbol') clickable as well</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Ability to resize 'Transformer Core' schematic symbol after it's already placed</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>Clicking on 'Tactile Micro Switch' body doesn't register</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>BUG_FIX</changeType>
|
||||
<description>New lines at the end of the 'Label' text get erroneously rendered as br HTML tags</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
</a>
|
||||
</java.util.Arrays_-ArrayList>
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.diylc.components.AbstractCurvedComponent.PointCount;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
|
||||
public class AbstractCurvedComponentTest {
|
||||
|
||||
private static final double DELTA = 1e-6;
|
||||
|
||||
@Test
|
||||
public void stretchBetween_putsTheEndsExactlyOnTheSuppliedPoints() {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.stretchBetween(new Point2D.Double(10, 20), new Point2D.Double(110, 20));
|
||||
assertPoint(10, 20, c.getControlPoint(0));
|
||||
assertPoint(110, 20, c.getControlPoint(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_movesTheIntermediatePointsOffTheChord() {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
assertTrue("first handle should not sit on the chord",
|
||||
Math.abs(c.getControlPoint(1).getY()) > 1);
|
||||
assertTrue("second handle should not sit on the chord",
|
||||
Math.abs(c.getControlPoint(2).getY()) > 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_putsConsecutivePointsOnOppositeSides() {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
assertTrue("the first excursion should point down the screen", c.getControlPoint(1).getY() > 0);
|
||||
assertTrue("the second excursion should point the other way", c.getControlPoint(2).getY() < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_alternatesSidesForEveryPointCount() {
|
||||
for (PointCount pointCount : PointCount.values()) {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.setPointCount(pointCount);
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
double previous = 0;
|
||||
for (int i = 1; i < c.getControlPointCount() - 1; i++) {
|
||||
double current = c.getControlPoint(i).getY();
|
||||
// points that lie on the curve stay on the chord and do not take part in the alternation
|
||||
if (Math.abs(current) < DELTA) {
|
||||
continue;
|
||||
}
|
||||
if (previous != 0) {
|
||||
assertTrue(pointCount + " handle " + i + " should sit opposite the previous one",
|
||||
previous * current < 0);
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_keepsTheOnCurvePointsOnTheStraightLine() {
|
||||
for (PointCount pointCount : new PointCount[] {PointCount.FIVE, PointCount.SEVEN}) {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.setPointCount(pointCount);
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
assertEquals(pointCount + " should keep its middle point on the chord", 0,
|
||||
c.getControlPoint(middleIndex(pointCount)).getY(), DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The smoothing in {@code draw} mirrors the handles around a point that lies on the curve, and
|
||||
* rewrites one of them when they are not already mirrored. Placing them mirrored to begin with
|
||||
* is what keeps that pass from pulling the freshly created wave out of shape.
|
||||
*/
|
||||
@Test
|
||||
public void stretchBetween_placesHandlesTheWayTheSmoothingRuleWants() {
|
||||
for (PointCount pointCount : new PointCount[] {PointCount.FIVE, PointCount.SEVEN}) {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.setPointCount(pointCount);
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 60));
|
||||
int middle = middleIndex(pointCount);
|
||||
Point2D before = c.getControlPoint(middle - 1);
|
||||
Point2D on = c.getControlPoint(middle);
|
||||
Point2D after = c.getControlPoint(middle + 1);
|
||||
assertEquals(pointCount + " handles should mirror horizontally", 2 * on.getX() - before.getX(),
|
||||
after.getX(), DELTA);
|
||||
assertEquals(pointCount + " handles should mirror vertically", 2 * on.getY() - before.getY(),
|
||||
after.getY(), DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
private static int middleIndex(PointCount pointCount) {
|
||||
return pointCount == PointCount.FIVE ? 2 : 3;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_waveIsSymmetricAboutTheMiddle() {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
assertEquals(-c.getControlPoint(1).getY(), c.getControlPoint(2).getY(), DELTA);
|
||||
assertEquals(100 - c.getControlPoint(2).getX(), c.getControlPoint(1).getX(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_wavesTheSameWayRegardlessOfDrawingDirection() {
|
||||
CurvedTestComponent forward = new CurvedTestComponent();
|
||||
forward.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
CurvedTestComponent backward = new CurvedTestComponent();
|
||||
backward.stretchBetween(new Point2D.Double(100, 0), new Point2D.Double(0, 0));
|
||||
for (int i = 1; i < forward.getControlPointCount() - 1; i++) {
|
||||
assertEquals(forward.getControlPoint(i).getY(), backward.getControlPoint(i).getY(), DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_scalesTheWaveWithTheDistance() {
|
||||
CurvedTestComponent shortWire = new CurvedTestComponent();
|
||||
shortWire.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(50, 0));
|
||||
CurvedTestComponent longWire = new CurvedTestComponent();
|
||||
longWire.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(200, 0));
|
||||
assertEquals(4 * shortWire.getControlPoint(1).getY(), longWire.getControlPoint(1).getY(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_collapsesOntoASinglePointWithoutDividingByZero() {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.stretchBetween(new Point2D.Double(40, 40), new Point2D.Double(40, 40));
|
||||
for (int i = 0; i < c.getControlPointCount(); i++) {
|
||||
assertPoint(40, 40, c.getControlPoint(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_keepsTheEndsForEveryPointCount() {
|
||||
for (PointCount pointCount : PointCount.values()) {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.setPointCount(pointCount);
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 60));
|
||||
assertPoint(0, 0, c.getControlPoint(0));
|
||||
assertPoint(100, 60, c.getControlPoint(c.getControlPointCount() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPoint(double x, double y, Point2D actual) {
|
||||
assertEquals(x, actual.getX(), DELTA);
|
||||
assertEquals(y, actual.getY(), DELTA);
|
||||
}
|
||||
|
||||
private static class CurvedTestComponent extends AbstractCurvedComponent<Void> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
protected void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver) {}
|
||||
|
||||
@Override
|
||||
protected Color getDefaultColor() {
|
||||
return Color.black;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {}
|
||||
}
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.diylc.common.OrientationHV;
|
||||
import org.diylc.components.AbstractOrthogonalComponent.PointCount;
|
||||
import org.diylc.components.transform.OrthogonalComponentTransformer;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
|
||||
public class AbstractOrthogonalComponentTest {
|
||||
|
||||
private static final double DELTA = 1e-6;
|
||||
|
||||
// --- routing ---
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOnEndRow_producesHorizontalVerticalHorizontal() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 50);
|
||||
assertRoute(c, 0, 0, 60, 0, 60, 50, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOnStartColumn_producesVerticalHorizontalVertical() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 0, 30);
|
||||
assertRoute(c, 0, 0, 0, 30, 100, 30, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendInsideTheBox_producesThreeBends() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
assertRoute(c, 0, 0, 60, 0, 60, 20, 100, 20, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOnBoxCorner_producesSingleBend() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 100, 0);
|
||||
assertRoute(c, 0, 0, 100, 0, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOnOppositeBoxCorner_leavesVerticallyAndArrivesHorizontally() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 0, 50);
|
||||
assertRoute(c, 0, 0, 0, 50, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOutsideTheBox_detoursAround() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, -40, 90);
|
||||
assertRoute(c, 0, 0, -40, 0, -40, 90, 100, 90, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_verticalStart_swapsTheAxes() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
c.setStartDirection(OrientationHV.VERTICAL);
|
||||
assertRoute(c, 0, 0, 0, 20, 60, 20, 60, 50, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_collapsedRoute_keepsASingleVertex() {
|
||||
OrthogonalTestComponent c = component(10, 10, 10, 10, 10, 10);
|
||||
assertRoute(c, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_everySegmentIsAxisAligned() {
|
||||
for (OrientationHV startDirection : OrientationHV.values()) {
|
||||
for (double x = -50; x <= 150; x += 25) {
|
||||
for (double y = -50; y <= 150; y += 25) {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, x, y);
|
||||
c.setStartDirection(startDirection);
|
||||
assertAxisAligned(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- point count ---
|
||||
|
||||
@Test
|
||||
public void setPointCount_keepsTheEndsAndStaysAxisAligned() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 50);
|
||||
c.setPointCount(PointCount.FIVE);
|
||||
assertEquals(5, c.getControlPointCount());
|
||||
assertPoint(0, 0, c.getControlPoint(0));
|
||||
assertPoint(100, 50, c.getControlPoint(4));
|
||||
assertAxisAligned(c);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setPointCount_backAndForth_restoresTheEnds() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 50);
|
||||
c.setPointCount(PointCount.FOUR);
|
||||
c.setPointCount(PointCount.THREE);
|
||||
assertEquals(3, c.getControlPointCount());
|
||||
assertPoint(0, 0, c.getControlPoint(0));
|
||||
assertPoint(100, 50, c.getControlPoint(2));
|
||||
}
|
||||
|
||||
// --- stretching ---
|
||||
|
||||
@Test
|
||||
public void stretchBetween_keepsTheEndsAndStaysAxisAligned() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
c.stretchBetween(new Point2D.Double(10, 10), new Point2D.Double(210, 110));
|
||||
assertPoint(10, 10, c.getControlPoint(0));
|
||||
assertPoint(210, 110, c.getControlPoint(2));
|
||||
assertAxisAligned(c);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_placesTheBendsWhereSetPointCountWould() {
|
||||
OrthogonalTestComponent stretched = component(0, 0, 100, 50, 60, 20);
|
||||
stretched.setPointCount(PointCount.FIVE);
|
||||
stretched.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 50));
|
||||
|
||||
OrthogonalTestComponent expanded = component(0, 0, 100, 50, 60, 20);
|
||||
expanded.setPointCount(PointCount.FIVE);
|
||||
|
||||
for (int i = 0; i < expanded.getControlPointCount(); i++) {
|
||||
assertPoint(expanded.getControlPoint(i).getX(), expanded.getControlPoint(i).getY(),
|
||||
stretched.getControlPoint(i));
|
||||
}
|
||||
}
|
||||
|
||||
// --- transformer ---
|
||||
|
||||
@Test
|
||||
public void rotate_producesTheRotatedRoute() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
AffineTransform rotate = AffineTransform.getRotateInstance(Math.PI / 2, 0, 0);
|
||||
List<Point2D> expected = new ArrayList<Point2D>();
|
||||
for (Point2D p : c.buildVertices()) {
|
||||
expected.add(rotate.transform(p, null));
|
||||
}
|
||||
|
||||
new OrthogonalComponentTransformer().rotate(c, new Point2D.Double(0, 0), 1);
|
||||
|
||||
assertEquals(OrientationHV.VERTICAL, c.getStartDirection());
|
||||
List<Point2D> actual = c.buildVertices();
|
||||
assertEquals(expected.size(), actual.size());
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
assertPoint(expected.get(i).getX(), expected.get(i).getY(), actual.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mirror_keepsTheStartDirection() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
new OrthogonalComponentTransformer().mirror(c, new Point2D.Double(50, 25), 1);
|
||||
assertEquals(OrientationHV.HORIZONTAL, c.getStartDirection());
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
private static OrthogonalTestComponent component(double startX, double startY, double endX, double endY,
|
||||
double bendX, double bendY) {
|
||||
OrthogonalTestComponent c = new OrthogonalTestComponent();
|
||||
c.setControlPoint(new Point2D.Double(startX, startY), 0);
|
||||
c.setControlPoint(new Point2D.Double(bendX, bendY), 1);
|
||||
c.setControlPoint(new Point2D.Double(endX, endY), 2);
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void assertRoute(OrthogonalTestComponent c, double... expected) {
|
||||
List<Point2D> vertices = c.buildVertices();
|
||||
assertEquals(expected.length / 2, vertices.size());
|
||||
for (int i = 0; i < vertices.size(); i++) {
|
||||
assertPoint(expected[2 * i], expected[2 * i + 1], vertices.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPoint(double x, double y, Point2D actual) {
|
||||
assertEquals(x, actual.getX(), DELTA);
|
||||
assertEquals(y, actual.getY(), DELTA);
|
||||
}
|
||||
|
||||
private static void assertAxisAligned(OrthogonalTestComponent c) {
|
||||
List<Point2D> vertices = c.buildVertices();
|
||||
for (int i = 1; i < vertices.size(); i++) {
|
||||
Point2D previous = vertices.get(i - 1);
|
||||
Point2D current = vertices.get(i);
|
||||
boolean aligned = Math.abs(previous.getX() - current.getX()) < DELTA
|
||||
|| Math.abs(previous.getY() - current.getY()) < DELTA;
|
||||
assertTrue("segment " + previous + " to " + current + " is not axis aligned", aligned);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OrthogonalTestComponent extends AbstractOrthogonalComponent<Void> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
protected void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver) {}
|
||||
|
||||
@Override
|
||||
protected Color getDefaultColor() {
|
||||
return Color.black;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {}
|
||||
}
|
||||
}
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components.composite;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.diylc.common.Percentage;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.netlist.Node;
|
||||
import org.diylc.serialization.ProjectFileManager;
|
||||
import org.diylc.testcomponents.OnePointTestComponent;
|
||||
import org.diylc.testcomponents.TwoPointTestComponent;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Exercises the delegation, rigidity, cloning and equality behavior described in
|
||||
* {@code docs/plans/composite-building-blocks.md}.
|
||||
*/
|
||||
public class CompositeComponentTest {
|
||||
|
||||
private CompositeComponent twoChildComposite() {
|
||||
CompositeComponent composite = new CompositeComponent();
|
||||
composite.setName("BLK1");
|
||||
composite.initBlockName("Test Block");
|
||||
composite.getChildComponents().add(new TwoPointTestComponent("R1",
|
||||
new Point2D.Double(0, 0), new Point2D.Double(10, 0)));
|
||||
composite.getChildComponents().add(new TwoPointTestComponent("R2",
|
||||
new Point2D.Double(20, 0), new Point2D.Double(30, 0)));
|
||||
return composite;
|
||||
}
|
||||
|
||||
private CompositeComponent customBoard(IDIYComponent<?>... children) {
|
||||
CompositeComponent composite = new CompositeComponent();
|
||||
composite.setName("BLK1");
|
||||
composite.initBlockName("Custom Board");
|
||||
composite.getChildComponents().addAll(Arrays.asList(children));
|
||||
return composite;
|
||||
}
|
||||
|
||||
private OnePointTestComponent pad(String name, String nodeName) {
|
||||
return new OnePointTestComponent(name, nodeName, new Point2D.Double(0, 0));
|
||||
}
|
||||
|
||||
// Stands in for a blank board: its corner points keep the default "1" and "2" node names but
|
||||
// are not sticky.
|
||||
private TwoPointTestComponent blankBoard() {
|
||||
TwoPointTestComponent board = new TwoPointTestComponent("Board1", new Point2D.Double(0, 0),
|
||||
new Point2D.Double(100, 100));
|
||||
board.setSticky(0, false);
|
||||
board.setSticky(1, false);
|
||||
return board;
|
||||
}
|
||||
|
||||
private TwoPointTestComponent twoPoint(String name) {
|
||||
return new TwoPointTestComponent(name, new Point2D.Double(0, 0), new Point2D.Double(10, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controlPointCountIsSumOverChildren() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
assertEquals(4, composite.getControlPointCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controlPointsMapToTheRightChildAndLocalIndex() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
|
||||
assertEquals(new Point2D.Double(0, 0), composite.getControlPoint(0));
|
||||
assertEquals(new Point2D.Double(10, 0), composite.getControlPoint(1));
|
||||
assertEquals(new Point2D.Double(20, 0), composite.getControlPoint(2));
|
||||
assertEquals(new Point2D.Double(30, 0), composite.getControlPoint(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setControlPointDelegatesToTheOwningChild() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
|
||||
composite.setControlPoint(new Point2D.Double(99, 99), 2);
|
||||
|
||||
assertEquals(new Point2D.Double(99, 99), composite.getControlPoint(2));
|
||||
// the sibling child's points must be untouched
|
||||
assertEquals(new Point2D.Double(30, 0), composite.getControlPoint(3));
|
||||
TwoPointTestComponent r2 = (TwoPointTestComponent) composite.getChildComponents().get(1);
|
||||
assertEquals(new Point2D.Double(99, 99), r2.getControlPoint(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stickinessIsDelegatedPerChildPoint() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
((TwoPointTestComponent) composite.getChildComponents().get(0)).setSticky(1, false);
|
||||
|
||||
assertTrue(composite.isControlPointSticky(0));
|
||||
assertFalse(composite.isControlPointSticky(1));
|
||||
assertTrue(composite.isControlPointSticky(2));
|
||||
assertTrue(composite.isControlPointSticky(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isAlwaysRigid() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
for (int i = 0; i < composite.getControlPointCount(); i++)
|
||||
assertFalse("point " + i + " should not move freely", composite.canPointMoveFreely(i));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clashingPinsOfMultiPointChildrenAreQualifiedWithTheChildName() {
|
||||
CompositeComponent composite = twoChildComposite();
|
||||
|
||||
assertEquals("R1.1", composite.getControlPointNodeName(0));
|
||||
assertEquals("R1.2", composite.getControlPointNodeName(1));
|
||||
assertEquals("BLK1.R2.1", new Node(composite, 2).toString());
|
||||
// local point index must reset per child, not keep growing with the flat composite index
|
||||
assertEquals("BLK1.R2.2", new Node(composite, 3).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uniquePadNamesPassThroughWithoutThePadName() {
|
||||
CompositeComponent composite =
|
||||
customBoard(blankBoard(), pad("Pad3", "VCC"), pad("Pad7", "SDA"));
|
||||
|
||||
assertEquals("VCC", composite.getControlPointNodeName(2));
|
||||
assertEquals("SDA", composite.getControlPointNodeName(3));
|
||||
assertEquals("BLK1.VCC", new Node(composite, 2).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonStickyPointsDoNotForceARename() {
|
||||
CompositeComponent composite = customBoard(blankBoard(), pad("Pad1", "1"));
|
||||
|
||||
assertEquals("1", composite.getControlPointNodeName(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void padsSharingANameGetNumericSuffixesInSaveOrder() {
|
||||
CompositeComponent composite = customBoard(blankBoard(), pad("Pad1", "GND"),
|
||||
pad("Pad2", "VCC"), pad("Pad3", "GND"), pad("Pad4", "GND"));
|
||||
|
||||
assertEquals("GND_1", composite.getControlPointNodeName(2));
|
||||
assertEquals("VCC", composite.getControlPointNodeName(3));
|
||||
assertEquals("GND_2", composite.getControlPointNodeName(4));
|
||||
assertEquals("GND_3", composite.getControlPointNodeName(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void suffixesSkipNamesThatAreAlreadyTaken() {
|
||||
CompositeComponent composite =
|
||||
customBoard(pad("Pad1", "GND"), pad("Pad2", "GND_1"), pad("Pad3", "GND"));
|
||||
|
||||
assertEquals("GND_2", composite.getControlPointNodeName(0));
|
||||
assertEquals("GND_1", composite.getControlPointNodeName(1));
|
||||
assertEquals("GND_3", composite.getControlPointNodeName(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void padClashingWithAPinOfAMultiPointChildKeepsItsName() {
|
||||
CompositeComponent composite = customBoard(pad("Pad1", "1"), twoPoint("R1"));
|
||||
|
||||
assertEquals("1", composite.getControlPointNodeName(0));
|
||||
assertEquals("R1.1", composite.getControlPointNodeName(1));
|
||||
assertEquals("2", composite.getControlPointNodeName(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childrenSharingANameAreStillDistinguished() {
|
||||
// renaming in the property editor does not enforce uniqueness, so a block can hold two "R1"s
|
||||
CompositeComponent composite = customBoard(twoPoint("R1"), twoPoint("R1"));
|
||||
|
||||
assertEquals("R1.1_1", composite.getControlPointNodeName(0));
|
||||
assertEquals("R1.2_1", composite.getControlPointNodeName(1));
|
||||
assertEquals("R1.1_2", composite.getControlPointNodeName(2));
|
||||
assertEquals("R1.2_2", composite.getControlPointNodeName(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unnamedPointsAreNotTerminals() {
|
||||
CompositeComponent composite =
|
||||
customBoard(pad("Pad1", null), pad("Pad2", null), pad("Pad3", "VCC"));
|
||||
|
||||
assertNull(composite.getControlPointNodeName(0));
|
||||
assertNull(composite.getControlPointNodeName(1));
|
||||
assertEquals("VCC", composite.getControlPointNodeName(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void instancesOfTheSameBlockResolveIdenticalNames() throws Exception {
|
||||
CompositeComponent original = customBoard(blankBoard(), pad("Pad1", "GND"),
|
||||
pad("Pad2", "GND"), twoPoint("R1"), twoPoint("R2"));
|
||||
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
|
||||
for (int i = 0; i < original.getControlPointCount(); i++) {
|
||||
assertEquals(original.getControlPointNodeName(i), clone.getControlPointNodeName(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneDeepCopiesChildrenSoInstancesAreIndependent() throws Exception {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
|
||||
IDIYComponent<?> clone = original.clone();
|
||||
|
||||
((CompositeComponent) clone).setControlPoint(new Point2D.Double(-1, -1), 0);
|
||||
|
||||
assertEquals(new Point2D.Double(0, 0), original.getControlPoint(0));
|
||||
assertEquals(new Point2D.Double(-1, -1), clone.getControlPoint(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneKeepsBothItsOwnAndChildrenIds() throws Exception {
|
||||
// Matches AbstractComponent.clone()'s contract of preserving identity by default - a caller
|
||||
// that needs a genuinely new component (e.g. BuildingBlockManager placing a fresh instance)
|
||||
// reassigns ids itself, exactly as it does for any other component's clone().
|
||||
CompositeComponent original = twoChildComposite();
|
||||
UUID originalChildId = original.getChildComponents().get(0).getId();
|
||||
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
|
||||
assertEquals(original.getId(), clone.getId());
|
||||
assertEquals(originalChildId, clone.getChildComponents().get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsToComparesStructureNotObjectIdentity() throws Exception {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
|
||||
// structurally identical, distinct child objects - must still compare equal
|
||||
assertTrue(original.equalsTo(clone));
|
||||
|
||||
clone.setControlPoint(new Point2D.Double(-1, -1), 0);
|
||||
assertFalse(original.equalsTo(clone));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsToRejectsDifferentBlockName() {
|
||||
CompositeComponent a = twoChildComposite();
|
||||
CompositeComponent b = twoChildComposite();
|
||||
b.initBlockName("Different Block");
|
||||
|
||||
assertFalse(a.equalsTo(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultsToFullyOpaque() {
|
||||
assertEquals(new Percentage(100), twoChildComposite().getAlpha());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneCopiesAlpha() throws Exception {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
original.setAlpha(new Percentage(40));
|
||||
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
|
||||
assertEquals(new Percentage(40), clone.getAlpha());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsToRejectsDifferentAlpha() throws Exception {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
CompositeComponent clone = (CompositeComponent) original.clone();
|
||||
assertTrue(original.equalsTo(clone));
|
||||
|
||||
clone.setAlpha(new Percentage(50));
|
||||
assertFalse(original.equalsTo(clone));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void survivesAnXStreamRoundTripAndRebuildsItsIndexLazily() {
|
||||
CompositeComponent original = twoChildComposite();
|
||||
|
||||
String xml = ProjectFileManager.xStreamSerializer.toXML(original);
|
||||
CompositeComponent reloaded = (CompositeComponent) ProjectFileManager.xStreamSerializer.fromXML(xml);
|
||||
|
||||
// the transient child-index arrays are never serialized - this call exercises the lazy
|
||||
// rebuild in buildIndexIfNeeded() on an instance XStream constructed without going through
|
||||
// the no-arg constructor
|
||||
assertEquals(original.getControlPointCount(), reloaded.getControlPointCount());
|
||||
assertEquals(original.getName(), reloaded.getName());
|
||||
assertEquals(original.getBlockName(), reloaded.getBlockName());
|
||||
for (int i = 0; i < original.getControlPointCount(); i++)
|
||||
assertEquals(original.getControlPoint(i), reloaded.getControlPoint(i));
|
||||
assertEquals("BLK1.R2.2", new Node(reloaded, 3).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drawIconDoesNotTouchChildren() {
|
||||
// ComponentProcessor draws the palette icon on a default-constructed, childless instance
|
||||
CompositeComponent blank = new CompositeComponent();
|
||||
BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
|
||||
blank.drawIcon(g2d, 32, 32);
|
||||
|
||||
g2d.dispose();
|
||||
// no exception means components list (empty) was never dereferenced as if populated
|
||||
assertNotNull(blank.getChildComponents());
|
||||
assertTrue(blank.getChildComponents().isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.core.measures;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class SizeTest {
|
||||
|
||||
private static final double DELTA = 1e-9;
|
||||
|
||||
// --- convertToUnits: same unit ---
|
||||
|
||||
@Test
|
||||
public void convertToUnits_sameUnit_returnsNewSizeWithValueRoundedTo4Decimals() {
|
||||
Size size = new Size(1.23456789, SizeUnit.mm);
|
||||
Size result = size.convertToUnits(SizeUnit.mm);
|
||||
assertNotSame(size, result);
|
||||
assertEquals(SizeUnit.mm, result.getUnit());
|
||||
assertEquals(1.2346, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_sameUnit_roundsTo4Decimals() {
|
||||
assertEquals(2.3457, new Size(2.3456789, SizeUnit.cm).convertToUnits(SizeUnit.cm).getValue(), DELTA);
|
||||
assertEquals(0.5, new Size(0.5, SizeUnit.in).convertToUnits(SizeUnit.in).getValue(), DELTA);
|
||||
}
|
||||
|
||||
// --- convertToUnits: metric ↔ metric ---
|
||||
|
||||
@Test
|
||||
public void convertToUnits_mmToCm() {
|
||||
Size size = new Size(10.0, SizeUnit.mm);
|
||||
Size result = size.convertToUnits(SizeUnit.cm);
|
||||
assertEquals(SizeUnit.cm, result.getUnit());
|
||||
assertEquals(1.0, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_cmToMm() {
|
||||
Size size = new Size(2.5, SizeUnit.cm);
|
||||
Size result = size.convertToUnits(SizeUnit.mm);
|
||||
assertEquals(SizeUnit.mm, result.getUnit());
|
||||
assertEquals(25.0, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_mmToM() {
|
||||
Size size = new Size(1500.0, SizeUnit.mm);
|
||||
Size result = size.convertToUnits(SizeUnit.m);
|
||||
assertEquals(SizeUnit.m, result.getUnit());
|
||||
assertEquals(1.5, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_mToMm() {
|
||||
Size size = new Size(0.5, SizeUnit.m);
|
||||
Size result = size.convertToUnits(SizeUnit.mm);
|
||||
assertEquals(SizeUnit.mm, result.getUnit());
|
||||
assertEquals(500.0, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
// --- convertToUnits: imperial ↔ imperial ---
|
||||
|
||||
@Test
|
||||
public void convertToUnits_inToFt() {
|
||||
Size size = new Size(24.0, SizeUnit.in);
|
||||
Size result = size.convertToUnits(SizeUnit.ft);
|
||||
assertEquals(SizeUnit.ft, result.getUnit());
|
||||
assertEquals(2.0, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_ftToIn() {
|
||||
Size size = new Size(1.0, SizeUnit.ft);
|
||||
Size result = size.convertToUnits(SizeUnit.in);
|
||||
assertEquals(SizeUnit.in, result.getUnit());
|
||||
assertEquals(12.0, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
// --- convertToUnits: metric ↔ imperial ---
|
||||
|
||||
@Test
|
||||
public void convertToUnits_mmToIn() {
|
||||
Size size = new Size(25.4, SizeUnit.mm);
|
||||
Size result = size.convertToUnits(SizeUnit.in);
|
||||
assertEquals(SizeUnit.in, result.getUnit());
|
||||
assertEquals(1.0, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_inToMm() {
|
||||
Size size = new Size(1.0, SizeUnit.in);
|
||||
Size result = size.convertToUnits(SizeUnit.mm);
|
||||
assertEquals(SizeUnit.mm, result.getUnit());
|
||||
assertEquals(25.4, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_cmToIn() {
|
||||
Size size = new Size(2.54, SizeUnit.cm);
|
||||
Size result = size.convertToUnits(SizeUnit.in);
|
||||
assertEquals(SizeUnit.in, result.getUnit());
|
||||
assertEquals(1.0, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
// --- convertToUnits: rounding to 4 decimal places ---
|
||||
|
||||
@Test
|
||||
public void convertToUnits_resultIsRoundedTo4Decimals() {
|
||||
Size size = new Size(1.0, SizeUnit.mm);
|
||||
Size result = size.convertToUnits(SizeUnit.in);
|
||||
// 1 mm = 1/25.4 in ≈ 0.03937007874015748...
|
||||
assertEquals(0.0394, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_roundingDown() {
|
||||
Size size = new Size(1.23441, SizeUnit.mm);
|
||||
Size result = size.convertToUnits(SizeUnit.mm);
|
||||
assertEquals(1.2344, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToUnits_roundingUp() {
|
||||
Size size = new Size(1.23446, SizeUnit.mm);
|
||||
Size result = size.convertToUnits(SizeUnit.mm);
|
||||
assertEquals(1.2345, result.getValue(), DELTA);
|
||||
}
|
||||
|
||||
// --- parseSize ---
|
||||
|
||||
@Test
|
||||
public void parseSize_mm() {
|
||||
Size size = Size.parseSize("10.5mm");
|
||||
assertEquals(10.5, size.getValue(), DELTA);
|
||||
assertEquals(SizeUnit.mm, size.getUnit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSize_cm() {
|
||||
Size size = Size.parseSize("2.54cm");
|
||||
assertEquals(2.54, size.getValue(), DELTA);
|
||||
assertEquals(SizeUnit.cm, size.getUnit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSize_in() {
|
||||
Size size = Size.parseSize("1in");
|
||||
assertEquals(1.0, size.getValue(), DELTA);
|
||||
assertEquals(SizeUnit.in, size.getUnit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseSize_caseInsensitive() {
|
||||
Size size = Size.parseSize("5 MM");
|
||||
assertEquals(5.0, size.getValue(), DELTA);
|
||||
assertEquals(SizeUnit.mm, size.getUnit());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void parseSize_unknownUnit_throws() {
|
||||
Size.parseSize("10 xyz");
|
||||
}
|
||||
|
||||
// --- fromPixels ---
|
||||
|
||||
@Test
|
||||
public void fromPixels_roundTripWithConvertToPixels() {
|
||||
Size original = new Size(1.0, SizeUnit.in);
|
||||
double pixels = original.convertToPixels();
|
||||
Size fromPixels = Size.fromPixels(pixels, SizeUnit.in);
|
||||
assertEquals(original.getValue(), fromPixels.getValue(), 0.01);
|
||||
assertEquals(original.getUnit(), fromPixels.getUnit());
|
||||
}
|
||||
|
||||
// --- scale ---
|
||||
|
||||
@Test
|
||||
public void scale() {
|
||||
Size size = new Size(10.0, SizeUnit.mm);
|
||||
Size scaled = size.scale(2.0);
|
||||
assertEquals(20.0, scaled.getValue(), DELTA);
|
||||
assertEquals(SizeUnit.mm, scaled.getUnit());
|
||||
}
|
||||
|
||||
// --- compareTo ---
|
||||
|
||||
@Test
|
||||
public void compareTo_samePhysicalSize_returnsZero() {
|
||||
Size a = new Size(25.4, SizeUnit.mm);
|
||||
Size b = new Size(1.0, SizeUnit.in);
|
||||
assertEquals(0, a.compareTo(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compareTo_smaller_returnsNegative() {
|
||||
Size a = new Size(1.0, SizeUnit.mm);
|
||||
Size b = new Size(1.0, SizeUnit.cm);
|
||||
assertTrue(a.compareTo(b) < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compareTo_larger_returnsPositive() {
|
||||
Size a = new Size(10.0, SizeUnit.mm);
|
||||
Size b = new Size(1.0, SizeUnit.mm);
|
||||
assertTrue(a.compareTo(b) > 0);
|
||||
}
|
||||
|
||||
// --- clone ---
|
||||
|
||||
@Test
|
||||
public void clone_returnsEqualSize() throws CloneNotSupportedException {
|
||||
Size size = new Size(3.14, SizeUnit.cm);
|
||||
Size cloned = size.clone();
|
||||
assertNotSame(size, cloned);
|
||||
assertEquals(size.getValue(), cloned.getValue(), DELTA);
|
||||
assertEquals(size.getUnit(), cloned.getUnit());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
*
|
||||
* DIY Layout Creator (DIYLC).
|
||||
* Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
*
|
||||
* This file is part of DIYLC.
|
||||
*
|
||||
* DIYLC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* DIYLC is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.diylc.netlist;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class NodeNameTest {
|
||||
|
||||
@Test
|
||||
public void testSanitizeNodeName() {
|
||||
// The annotation in parentheses is stripped, the pin name is kept
|
||||
Assert.assertEquals("D11", Node.sanitizeNodeName("D11 (~, MOSI)"));
|
||||
Assert.assertEquals("A5", Node.sanitizeNodeName("A5 (SCL)"));
|
||||
Assert.assertEquals("D3", Node.sanitizeNodeName("D3 (~)"));
|
||||
Assert.assertEquals("VIN", Node.sanitizeNodeName("VIN (3.6-5.5V)"));
|
||||
Assert.assertEquals("GPIO2/SDA", Node.sanitizeNodeName("GPIO2/SDA (Pin 3)"));
|
||||
|
||||
// Names without an annotation are untouched
|
||||
Assert.assertEquals("VIN", Node.sanitizeNodeName("VIN"));
|
||||
Assert.assertEquals("Tip", Node.sanitizeNodeName("Tip"));
|
||||
Assert.assertEquals("3.3V", Node.sanitizeNodeName("3.3V"));
|
||||
|
||||
// Trailing disambiguators stay: these mark genuinely different pins
|
||||
Assert.assertEquals("GND_1", Node.sanitizeNodeName("GND_1"));
|
||||
Assert.assertEquals("5V_ICSP", Node.sanitizeNodeName("5V_ICSP"));
|
||||
|
||||
// Degenerate input never leaves a pin nameless
|
||||
Assert.assertEquals("(only annotation)", Node.sanitizeNodeName("(only annotation)"));
|
||||
Assert.assertNull(Node.sanitizeNodeName(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void railNameDropsOnlyANumericDisambiguator() {
|
||||
Assert.assertEquals("GND", Node.railName("GND_1"));
|
||||
Assert.assertEquals("GND", Node.railName("GND_12"));
|
||||
Assert.assertEquals("GND", Node.railName("GND"));
|
||||
Assert.assertEquals("GND", Node.railName("GND (Pin 6)"));
|
||||
Assert.assertEquals("+5V", Node.railName("+5V_2"));
|
||||
// the annotation is stripped before the disambiguator, so an annotated rail still resolves
|
||||
Assert.assertEquals("3.3V", Node.railName("3.3V_2 (250mA)"));
|
||||
Assert.assertEquals("GND", Node.railName("GND_4 (Mid)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void railNameKeepsNonNumericQualifiers() {
|
||||
// namespaced connector signals
|
||||
Assert.assertEquals("ETH_TX-", Node.railName("ETH_TX-"));
|
||||
Assert.assertEquals("USB_D+", Node.railName("USB_D+"));
|
||||
// qualifiers that name a separate net
|
||||
Assert.assertEquals("MISO_16U2", Node.railName("MISO_16U2"));
|
||||
Assert.assertEquals("3V3_EN", Node.railName("3V3_EN"));
|
||||
// a bare trailing digit is part of the name, not a disambiguator
|
||||
Assert.assertEquals("GP1", Node.railName("GP1"));
|
||||
Assert.assertEquals("GND1", Node.railName("GND1"));
|
||||
// digits that are not at the end are part of the name too: the 16U2 bridge's ground and 5V
|
||||
// are separate nets from the board's own, and USB_5V is a connector pin, not a 5V rail
|
||||
Assert.assertEquals("GND_16U2", Node.railName("GND_16U2"));
|
||||
Assert.assertEquals("5V_16U2", Node.railName("5V_16U2"));
|
||||
Assert.assertEquals("USB_5V", Node.railName("USB_5V"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void railNameRejectsPinsThatNameNoNet() {
|
||||
Assert.assertNull(Node.railName("NC_1"));
|
||||
Assert.assertNull(Node.railName("RSV_2"));
|
||||
Assert.assertNull(Node.railName(null));
|
||||
}
|
||||
}
|
||||
+90
-83
@@ -48,33 +48,34 @@ public class CompareServiceTest {
|
||||
|
||||
@Test
|
||||
public void testCompare_MissingConnectionsInSecondNetlist() {
|
||||
Map<String, IDIYComponent<?>> componentMap = new HashMap<>();
|
||||
// Group will have 3 nodes
|
||||
Netlist netlist1 = createNetlist(componentMap, "Component1", "Node1", "Component2", "Node2", "Component3", "Node3");
|
||||
Netlist netlist2 = createNetlist(componentMap, "Component1", "Node1"); // Missing group with 2 nodes
|
||||
Map<String, IDIYComponent<?>> componentMap1 = new HashMap<>();
|
||||
Netlist netlist1 = createNetlist(componentMap1, "Component1", "Node1", "Component2", "Node2", "Component3", "Node3");
|
||||
Map<String, IDIYComponent<?>> componentMap2 = new HashMap<>();
|
||||
Netlist netlist2 = createNetlist(componentMap2, "Component1", "Node1"); // Missing components 2 & 3
|
||||
|
||||
CompareResults results = compareService.compare(netlist1, netlist2);
|
||||
|
||||
assertTrue(!results.matches());
|
||||
// 2 components missing = 2 component diffs
|
||||
assertEquals(2, results.componentDiffs().size());
|
||||
// 3 nodes = 3 connections (1-2, 1-3, 2-3)
|
||||
assertEquals(3, results.connectionDiffs().size());
|
||||
// Connections involving missing components are filtered out, so 0 connection diffs
|
||||
assertEquals(0, results.connectionDiffs().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompare_MissingConnectionsInFirstNetlist() {
|
||||
Map<String, IDIYComponent<?>> componentMap = new HashMap<>();
|
||||
// Group will have 3 nodes
|
||||
Netlist netlist1 = createNetlist(componentMap, "Component1", "Node1");
|
||||
Netlist netlist2 = createNetlist(componentMap, "Component1", "Node1", "Component2", "Node2", "Component3", "Node3");
|
||||
Map<String, IDIYComponent<?>> componentMap1 = new HashMap<>();
|
||||
Netlist netlist1 = createNetlist(componentMap1, "Component1", "Node1");
|
||||
Map<String, IDIYComponent<?>> componentMap2 = new HashMap<>();
|
||||
Netlist netlist2 = createNetlist(componentMap2, "Component1", "Node1", "Component2", "Node2", "Component3", "Node3");
|
||||
|
||||
CompareResults results = compareService.compare(netlist1, netlist2);
|
||||
|
||||
assertTrue(!results.matches());
|
||||
// 2 components missing = 2 component diffs
|
||||
assertEquals(2, results.componentDiffs().size());
|
||||
assertEquals(3, results.connectionDiffs().size());
|
||||
// Connections involving missing components are filtered out, so 0 connection diffs
|
||||
assertEquals(0, results.connectionDiffs().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -89,8 +90,8 @@ public class CompareServiceTest {
|
||||
assertTrue(!results.matches());
|
||||
// 4 components missing (2 in each netlist) = 4 component diffs
|
||||
assertEquals(4, results.componentDiffs().size());
|
||||
// 2 connections in each netlist = 2 connection diffs
|
||||
assertEquals(2, results.connectionDiffs().size());
|
||||
// Connections involving missing components are filtered out, so 0 connection diffs
|
||||
assertEquals(0, results.connectionDiffs().size());
|
||||
|
||||
// Verify differences from first netlist
|
||||
List<ComponentDiff> firstNetlistComponentDiffs = results.componentDiffs().stream()
|
||||
@@ -109,13 +110,15 @@ public class CompareServiceTest {
|
||||
public void testCompare_MultipleGroups_Identical() {
|
||||
Map<String, IDIYComponent<?>> componentMap = new HashMap<>();
|
||||
|
||||
// Create first netlist with two groups
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>());
|
||||
netlist1.add(createGroup(componentMap, "Component1", "Node1", "Component2", "Node2"));
|
||||
netlist1.add(createGroup(componentMap, "Component3", "Node3", "Component4", "Node4"));
|
||||
// Create groups first so componentMap is populated
|
||||
Group g1a = createGroup(componentMap, "Component1", "Node1", "Component2", "Node2");
|
||||
Group g1b = createGroup(componentMap, "Component3", "Node3", "Component4", "Node4");
|
||||
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist1.add(g1a);
|
||||
netlist1.add(g1b);
|
||||
|
||||
// Create second netlist with same groups
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>());
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist2.add(createGroup(componentMap, "Component1", "Node1", "Component2", "Node2"));
|
||||
netlist2.add(createGroup(componentMap, "Component3", "Node3", "Component4", "Node4"));
|
||||
|
||||
@@ -130,13 +133,16 @@ public class CompareServiceTest {
|
||||
public void testCompare_MultipleGroups_DifferentOrder() {
|
||||
Map<String, IDIYComponent<?>> componentMap = new HashMap<>();
|
||||
|
||||
// Create first netlist with two groups
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>());
|
||||
netlist1.add(createGroup(componentMap, "Component1", "Node1", "Component2", "Node2"));
|
||||
netlist1.add(createGroup(componentMap, "Component3", "Node3", "Component4", "Node4"));
|
||||
// Create groups first so componentMap is populated
|
||||
Group g1a = createGroup(componentMap, "Component1", "Node1", "Component2", "Node2");
|
||||
Group g1b = createGroup(componentMap, "Component3", "Node3", "Component4", "Node4");
|
||||
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist1.add(g1a);
|
||||
netlist1.add(g1b);
|
||||
|
||||
// Create second netlist with same groups but in different order
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>());
|
||||
// Same groups but in different order
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist2.add(createGroup(componentMap, "Component3", "Node3", "Component4", "Node4"));
|
||||
netlist2.add(createGroup(componentMap, "Component1", "Node1", "Component2", "Node2"));
|
||||
|
||||
@@ -149,33 +155,45 @@ public class CompareServiceTest {
|
||||
|
||||
@Test
|
||||
public void testCompare_MultipleGroups_ExtraGroup() {
|
||||
Map<String, IDIYComponent<?>> componentMap = new HashMap<>();
|
||||
// Each group has 2 nodes
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>());
|
||||
netlist1.add(createGroup(componentMap, "Component1", "Node1", "Component2", "Node2"));
|
||||
netlist1.add(createGroup(componentMap, "Component3", "Node3", "Component4", "Node4"));
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>());
|
||||
netlist2.add(createGroup(componentMap, "Component1", "Node1", "Component2", "Node2"));
|
||||
netlist2.add(createGroup(componentMap, "Component3", "Node3", "Component4", "Node4"));
|
||||
netlist2.add(createGroup(componentMap, "Component5", "Node5", "Component6", "Node6"));
|
||||
Map<String, IDIYComponent<?>> componentMap1 = new HashMap<>();
|
||||
// Create shared groups first
|
||||
Group shared1 = createGroup(componentMap1, "Component1", "Node1", "Component2", "Node2");
|
||||
Group shared2 = createGroup(componentMap1, "Component3", "Node3", "Component4", "Node4");
|
||||
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>(componentMap1.values()));
|
||||
netlist1.add(shared1);
|
||||
netlist1.add(shared2);
|
||||
|
||||
Map<String, IDIYComponent<?>> componentMap2 = new HashMap<>();
|
||||
Group shared2_1 = createGroup(componentMap2, "Component1", "Node1", "Component2", "Node2");
|
||||
Group shared2_2 = createGroup(componentMap2, "Component3", "Node3", "Component4", "Node4");
|
||||
Group extra = createGroup(componentMap2, "Component5", "Node5", "Component6", "Node6");
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>(componentMap2.values()));
|
||||
netlist2.add(shared2_1);
|
||||
netlist2.add(shared2_2);
|
||||
netlist2.add(extra);
|
||||
|
||||
CompareResults results = compareService.compare(netlist1, netlist2);
|
||||
|
||||
assertTrue(!results.matches());
|
||||
// 2 components in extra group = 2 component diffs
|
||||
assertEquals(2, results.componentDiffs().size());
|
||||
// 1 connection in extra group = 1 connection diff
|
||||
assertEquals(1, results.connectionDiffs().size());
|
||||
// Connections to/from missing Component5/6 are filtered out, so 0 connection diffs
|
||||
assertEquals(0, results.connectionDiffs().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompare_MultipleGroups_DifferentConnections() {
|
||||
Map<String, IDIYComponent<?>> componentMap = new HashMap<>();
|
||||
// Each group has 2 nodes
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>());
|
||||
netlist1.add(createGroup(componentMap, "Component1", "Node1", "Component2", "Node2"));
|
||||
netlist1.add(createGroup(componentMap, "Component3", "Node3", "Component4", "Node4"));
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>());
|
||||
// Create groups first so componentMap is populated
|
||||
Group g1a = createGroup(componentMap, "Component1", "Node1", "Component2", "Node2");
|
||||
Group g1b = createGroup(componentMap, "Component3", "Node3", "Component4", "Node4");
|
||||
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist1.add(g1a);
|
||||
netlist1.add(g1b);
|
||||
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist2.add(createGroup(componentMap, "Component1", "Node1", "Component3", "Node3"));
|
||||
netlist2.add(createGroup(componentMap, "Component2", "Node2", "Component4", "Node4"));
|
||||
|
||||
@@ -191,20 +209,19 @@ public class CompareServiceTest {
|
||||
public void testCompare_NonPolarizedComponent_SwappedNodes() {
|
||||
Map<String, IDIYComponent<?>> componentMap = new HashMap<>();
|
||||
|
||||
// Create first netlist with non-polarized component in two groups
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>());
|
||||
netlist1.add(createGroup(componentMap, "Resistor1", "Node1", "Component2", "Node2"));
|
||||
netlist1.add(createGroup(componentMap, "Resistor1", "Node2", "Component3", "Node3"));
|
||||
// Create groups first so componentMap is populated
|
||||
Group g1a = createGroup(componentMap, "Resistor1", "Node1", "Component2", "Node2");
|
||||
Group g1b = createGroup(componentMap, "Resistor1", "Node2", "Component3", "Node3");
|
||||
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist1.add(g1a);
|
||||
netlist1.add(g1b);
|
||||
|
||||
// Create second netlist with same connections but different node order
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>());
|
||||
// Same connections but different node order
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist2.add(createGroup(componentMap, "Resistor1", "Node2", "Component2", "Node2"));
|
||||
netlist2.add(createGroup(componentMap, "Resistor1", "Node1", "Component3", "Node3"));
|
||||
|
||||
// Make Resistor1 non-polarized
|
||||
IDIYComponent<?> resistor = componentMap.get("Resistor1");
|
||||
when(resistor.isPolarized()).thenReturn(false);
|
||||
|
||||
CompareResults results = compareService.compare(netlist1, netlist2);
|
||||
|
||||
assertTrue(results.matches());
|
||||
@@ -214,9 +231,6 @@ public class CompareServiceTest {
|
||||
|
||||
@Test
|
||||
public void testCompare_PolarizedComponent_SwappedNodes() {
|
||||
// Create first netlist with polarized component in two groups
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>());
|
||||
|
||||
// Create diode for netlist1
|
||||
IDIYComponent<?> diode1 = mock(IDIYComponent.class);
|
||||
when(diode1.getName()).thenReturn("Diode1");
|
||||
@@ -225,7 +239,6 @@ public class CompareServiceTest {
|
||||
when(diode1.getControlPoint(1)).thenReturn(new Point2D.Double(0, 0));
|
||||
when(diode1.isControlPointSticky(0)).thenReturn(true);
|
||||
when(diode1.isControlPointSticky(1)).thenReturn(true);
|
||||
when(diode1.isPolarized()).thenReturn(true);
|
||||
when(diode1.toString()).thenReturn("Diode1");
|
||||
when(diode1.getControlPointNodeName(0)).thenReturn("Anode");
|
||||
when(diode1.getControlPointNodeName(1)).thenReturn("Cathode");
|
||||
@@ -238,7 +251,6 @@ public class CompareServiceTest {
|
||||
when(component2_1.getControlPoint(0)).thenReturn(new Point2D.Double(0, 0));
|
||||
when(component2_1.isControlPointSticky(0)).thenReturn(true);
|
||||
when(component2_1.toString()).thenReturn("Component2");
|
||||
when(component2_1.isPolarized()).thenReturn(false);
|
||||
|
||||
// Create Component3 for netlist1
|
||||
IDIYComponent<?> component3_1 = mock(IDIYComponent.class);
|
||||
@@ -248,8 +260,10 @@ public class CompareServiceTest {
|
||||
when(component3_1.getControlPoint(0)).thenReturn(new Point2D.Double(0, 0));
|
||||
when(component3_1.isControlPointSticky(0)).thenReturn(true);
|
||||
when(component3_1.toString()).thenReturn("Component3");
|
||||
when(component3_1.isPolarized()).thenReturn(false);
|
||||
|
||||
// Create first netlist with polarized component in two groups
|
||||
Netlist netlist1 = new Netlist(List.of(diode1, component2_1, component3_1));
|
||||
|
||||
// Create groups for netlist1
|
||||
Group group1 = new Group();
|
||||
Node node1 = new Node(diode1, 0); // Anode
|
||||
@@ -265,9 +279,6 @@ public class CompareServiceTest {
|
||||
group2.getNodes().add(node4);
|
||||
netlist1.add(group2);
|
||||
|
||||
// Create second netlist with same connections but different node order
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>());
|
||||
|
||||
// Create diode for netlist2
|
||||
IDIYComponent<?> diode2 = mock(IDIYComponent.class);
|
||||
when(diode2.getName()).thenReturn("Diode1");
|
||||
@@ -276,7 +287,6 @@ public class CompareServiceTest {
|
||||
when(diode2.getControlPoint(1)).thenReturn(new Point2D.Double(0, 0));
|
||||
when(diode2.isControlPointSticky(0)).thenReturn(true);
|
||||
when(diode2.isControlPointSticky(1)).thenReturn(true);
|
||||
when(diode2.isPolarized()).thenReturn(true);
|
||||
when(diode2.toString()).thenReturn("Diode1");
|
||||
when(diode2.getControlPointNodeName(0)).thenReturn("Anode");
|
||||
when(diode2.getControlPointNodeName(1)).thenReturn("Cathode");
|
||||
@@ -289,7 +299,6 @@ public class CompareServiceTest {
|
||||
when(component2_2.getControlPoint(0)).thenReturn(new Point2D.Double(0, 0));
|
||||
when(component2_2.isControlPointSticky(0)).thenReturn(true);
|
||||
when(component2_2.toString()).thenReturn("Component2");
|
||||
when(component2_2.isPolarized()).thenReturn(false);
|
||||
|
||||
// Create Component3 for netlist2
|
||||
IDIYComponent<?> component3_2 = mock(IDIYComponent.class);
|
||||
@@ -299,8 +308,10 @@ public class CompareServiceTest {
|
||||
when(component3_2.getControlPoint(0)).thenReturn(new Point2D.Double(0, 0));
|
||||
when(component3_2.isControlPointSticky(0)).thenReturn(true);
|
||||
when(component3_2.toString()).thenReturn("Component3");
|
||||
when(component3_2.isPolarized()).thenReturn(false);
|
||||
|
||||
// Create second netlist with same connections but different node order
|
||||
Netlist netlist2 = new Netlist(List.of(diode2, component2_2, component3_2));
|
||||
|
||||
// Create groups for netlist2 with swapped nodes
|
||||
Group group3 = new Group();
|
||||
Node node5 = new Node(diode2, 1); // Cathode
|
||||
@@ -322,8 +333,7 @@ public class CompareServiceTest {
|
||||
System.out.println("Group: " + g);
|
||||
for (Node n : g.getNodes()) {
|
||||
System.out.println(" Node: " + n.getComponent().getName() +
|
||||
" " + n.getComponent().getControlPointNodeName(n.getPointIndex()) +
|
||||
" (polarized: " + n.getComponent().isPolarized() + ")");
|
||||
" " + n.getComponent().getControlPointNodeName(n.getPointIndex()));
|
||||
}
|
||||
}
|
||||
System.out.println("\nNetlist2 groups:");
|
||||
@@ -331,8 +341,7 @@ public class CompareServiceTest {
|
||||
System.out.println("Group: " + g);
|
||||
for (Node n : g.getNodes()) {
|
||||
System.out.println(" Node: " + n.getComponent().getName() +
|
||||
" " + n.getComponent().getControlPointNodeName(n.getPointIndex()) +
|
||||
" (polarized: " + n.getComponent().isPolarized() + ")");
|
||||
" " + n.getComponent().getControlPointNodeName(n.getPointIndex()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,24 +456,23 @@ public class CompareServiceTest {
|
||||
public void testCompare_MixedPolarizedAndNonPolarized() {
|
||||
Map<String, IDIYComponent<?>> componentMap = new HashMap<>();
|
||||
|
||||
// Create first netlist with both polarized and non-polarized components
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>());
|
||||
netlist1.add(createGroup(componentMap, "Resistor1", "Node1", "Diode1", "Anode"));
|
||||
netlist1.add(createGroup(componentMap, "Resistor1", "Node2", "Component3", "Node3"));
|
||||
netlist1.add(createGroup(componentMap, "Diode1", "Cathode", "Component4", "Node4"));
|
||||
// Create groups first so componentMap is populated
|
||||
Group g1a = createGroup(componentMap, "Resistor1", "Node1", "Diode1", "Anode");
|
||||
Group g1b = createGroup(componentMap, "Resistor1", "Node2", "Component3", "Node3");
|
||||
Group g1c = createGroup(componentMap, "Diode1", "Cathode", "Component4", "Node4");
|
||||
|
||||
Netlist netlist1 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist1.add(g1a);
|
||||
netlist1.add(g1b);
|
||||
netlist1.add(g1c);
|
||||
|
||||
// Create second netlist with same connections but different node order for non-polarized
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>());
|
||||
// Same connections but different node order for non-polarized
|
||||
Netlist netlist2 = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist2.add(createGroup(componentMap, "Resistor1", "Node2", "Diode1", "Anode"));
|
||||
netlist2.add(createGroup(componentMap, "Resistor1", "Node1", "Component3", "Node3"));
|
||||
netlist2.add(createGroup(componentMap, "Diode1", "Cathode", "Component4", "Node4"));
|
||||
|
||||
// Make Resistor1 non-polarized and Diode1 polarized
|
||||
IDIYComponent<?> resistor = componentMap.get("Resistor1");
|
||||
IDIYComponent<?> diode = componentMap.get("Diode1");
|
||||
when(resistor.isPolarized()).thenReturn(false);
|
||||
when(diode.isPolarized()).thenReturn(true);
|
||||
|
||||
CompareResults results = compareService.compare(netlist1, netlist2);
|
||||
|
||||
assertTrue(results.matches());
|
||||
@@ -473,8 +481,9 @@ public class CompareServiceTest {
|
||||
}
|
||||
|
||||
private Netlist createNetlist(Map<String, IDIYComponent<?>> componentMap, String... componentNodePairs) {
|
||||
Netlist netlist = new Netlist(new ArrayList<>());
|
||||
netlist.add(createGroup(componentMap, componentNodePairs));
|
||||
Group group = createGroup(componentMap, componentNodePairs);
|
||||
Netlist netlist = new Netlist(new ArrayList<>(componentMap.values()));
|
||||
netlist.add(group);
|
||||
return netlist;
|
||||
}
|
||||
|
||||
@@ -493,8 +502,6 @@ public class CompareServiceTest {
|
||||
when(c.getControlPoint(0)).thenReturn(new Point2D.Double(0, 0));
|
||||
when(c.isControlPointSticky(0)).thenReturn(true);
|
||||
when(c.toString()).thenReturn(name);
|
||||
// Default to non-polarized
|
||||
when(c.isPolarized()).thenReturn(false);
|
||||
return c;
|
||||
});
|
||||
// For the same component name, update the node name for this instance
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||
import org.diylc.common.BlockInstantiationMode;
|
||||
import org.diylc.common.IBlockProcessor;
|
||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.components.composite.CompositeComponent;
|
||||
import org.diylc.core.ISwitch;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.testcomponents.SwitchTestComponent;
|
||||
import org.diylc.testcomponents.TwoPointTestComponent;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Exercises {@link BuildingBlockManager#loadBlock} in both {@link BlockInstantiationMode}s (see
|
||||
* design decision D10 and section 4.4 of {@code docs/plans/composite-building-blocks.md}).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class BuildingBlockManagerTest {
|
||||
|
||||
private static final String BLOCK_NAME = "TestBlock";
|
||||
|
||||
private IConfigurationManager<?> configManager;
|
||||
private BuildingBlockManager manager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
List<IDIYComponent<?>> storedComponents = Arrays.asList(
|
||||
new TwoPointTestComponent("R1", new Point2D.Double(0, 0), new Point2D.Double(10, 0)),
|
||||
new TwoPointTestComponent("R2", new Point2D.Double(20, 0), new Point2D.Double(30, 0)));
|
||||
Map<String, List<IDIYComponent<?>>> blocksMap = new HashMap<String, List<IDIYComponent<?>>>();
|
||||
blocksMap.put(BLOCK_NAME, storedComponents);
|
||||
|
||||
configManager = mock(IConfigurationManager.class);
|
||||
when(configManager.readObject(eq(IPlugInPort.BLOCKS_KEY), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(blocksMap);
|
||||
|
||||
manager = new BuildingBlockManager(configManager, null, new InstantiationManager());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groupModeClonesEachComponentWithAFreshNameAndId() throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> result =
|
||||
manager.loadBlock(BLOCK_NAME, new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.GROUP);
|
||||
|
||||
assertEquals(2, result.size());
|
||||
// group mode always assigns fresh, project-unique names (pre-existing behavior, unchanged)
|
||||
assertEquals("TP1", result.get(0).getName());
|
||||
assertEquals("TP2", result.get(1).getName());
|
||||
assertNotEquals(result.get(0).getId(), result.get(1).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeModeWrapsChildrenInASingleComponent() throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> result =
|
||||
manager.loadBlock(BLOCK_NAME, new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.COMPOSITE);
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertTrue(result.get(0) instanceof CompositeComponent);
|
||||
CompositeComponent composite = (CompositeComponent) result.get(0);
|
||||
|
||||
assertEquals(BLOCK_NAME, composite.getBlockName());
|
||||
assertEquals("BLK1", composite.getName());
|
||||
assertEquals(4, composite.getControlPointCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeModePreservesChildNamesUnlikeGroupMode() throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> result =
|
||||
manager.loadBlock(BLOCK_NAME, new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.COMPOSITE);
|
||||
CompositeComponent composite = (CompositeComponent) result.get(0);
|
||||
|
||||
// children are namespaced under the composite's own name in the netlist, so their saved
|
||||
// names must survive untouched
|
||||
assertEquals("R1", composite.getChildComponents().get(0).getName());
|
||||
assertEquals("R2", composite.getChildComponents().get(1).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeModeGivesEachInstanceItsOwnNameButChildrenStayIdenticallyNamed()
|
||||
throws InvalidBlockException {
|
||||
List<IDIYComponent<?>> existing = new ArrayList<IDIYComponent<?>>();
|
||||
CompositeComponent first = (CompositeComponent) manager
|
||||
.loadBlock(BLOCK_NAME, existing, BlockInstantiationMode.COMPOSITE).get(0);
|
||||
existing.add(first);
|
||||
|
||||
CompositeComponent second = (CompositeComponent) manager
|
||||
.loadBlock(BLOCK_NAME, existing, BlockInstantiationMode.COMPOSITE).get(0);
|
||||
|
||||
assertNotEquals(first.getName(), second.getName());
|
||||
assertEquals(first.getChildComponents().get(0).getName(), second.getChildComponents().get(0).getName());
|
||||
// but the underlying child components must be distinct objects with distinct ids
|
||||
assertNotEquals(first.getChildComponents().get(0).getId(), second.getChildComponents().get(0).getId());
|
||||
}
|
||||
|
||||
@Test(expected = InvalidBlockException.class)
|
||||
public void groupModeThrowsOnUnknownBlock() throws InvalidBlockException {
|
||||
manager.loadBlock("NoSuchBlock", new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.GROUP);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidBlockException.class)
|
||||
public void compositeModeThrowsOnUnknownBlock() throws InvalidBlockException {
|
||||
manager.loadBlock("NoSuchBlock", new ArrayList<IDIYComponent<?>>(), BlockInstantiationMode.COMPOSITE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteBlockAlsoStripsItFromTheRecentlyUsedList() {
|
||||
List<String> recent = new ArrayList<String>(Arrays.asList(
|
||||
IBlockProcessor.BLOCK_PREFIX + BLOCK_NAME, "org.diylc.components.SomeComponent"));
|
||||
when(configManager.readObject(eq(IPlugInPort.RECENT_COMPONENTS_KEY),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(recent);
|
||||
|
||||
manager.deleteBlock(BLOCK_NAME);
|
||||
|
||||
assertFalse(recent.contains(IBlockProcessor.BLOCK_PREFIX + BLOCK_NAME));
|
||||
assertTrue(recent.contains("org.diylc.components.SomeComponent"));
|
||||
verify(configManager).writeValue(eq(IPlugInPort.RECENT_COMPONENTS_KEY), eq(recent));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositeModeToleratesAnInternalSwitchWithoutFailing() throws InvalidBlockException {
|
||||
// Design decision D6: the composite is not ISwitch, so a block containing a switch must
|
||||
// still place successfully - it just won't switch (see BuildingBlockManager's log warning).
|
||||
List<IDIYComponent<?>> storedComponents = Arrays.<IDIYComponent<?>>asList(new SwitchTestComponent("SW1"));
|
||||
Map<String, List<IDIYComponent<?>>> blocksMap = new HashMap<String, List<IDIYComponent<?>>>();
|
||||
blocksMap.put("SwitchBlock", storedComponents);
|
||||
when(configManager.readObject(eq(IPlugInPort.BLOCKS_KEY), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(blocksMap);
|
||||
|
||||
List<IDIYComponent<?>> result = manager.loadBlock("SwitchBlock", new ArrayList<IDIYComponent<?>>(),
|
||||
BlockInstantiationMode.COMPOSITE);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertTrue(((CompositeComponent) result.get(0)).getChildComponents().get(0) instanceof ISwitch);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.diylc.presenter;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -142,4 +143,116 @@ public class CalcUtilsTest {
|
||||
point2.setLocation(10.0, 21.0);
|
||||
assertFalse(CalcUtils.pointsMatch(point1, point2, 0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateLuminance() {
|
||||
// Test pure black
|
||||
assertEquals(0.0, CalcUtils.calculateLuminance(Color.BLACK), 0.001);
|
||||
|
||||
// Test pure white
|
||||
assertEquals(255.0, CalcUtils.calculateLuminance(Color.WHITE), 0.001);
|
||||
|
||||
// Test pure red
|
||||
double redLuminance = CalcUtils.calculateLuminance(Color.RED);
|
||||
assertEquals(76.245, redLuminance, 0.001); // 0.299 * 255
|
||||
|
||||
// Test pure green
|
||||
double greenLuminance = CalcUtils.calculateLuminance(Color.GREEN);
|
||||
assertEquals(149.685, greenLuminance, 0.001); // 0.587 * 255
|
||||
|
||||
// Test pure blue
|
||||
double blueLuminance = CalcUtils.calculateLuminance(Color.BLUE);
|
||||
assertEquals(29.07, blueLuminance, 0.001); // 0.114 * 255
|
||||
|
||||
// Test middle gray (128, 128, 128)
|
||||
Color gray = new Color(128, 128, 128);
|
||||
double grayLuminance = CalcUtils.calculateLuminance(gray);
|
||||
assertEquals(128.0, grayLuminance, 0.001);
|
||||
|
||||
// Test null color
|
||||
assertEquals(0.0, CalcUtils.calculateLuminance(null), 0.001);
|
||||
|
||||
// Test custom color
|
||||
Color custom = new Color(100, 150, 200);
|
||||
double customLuminance = CalcUtils.calculateLuminance(custom);
|
||||
double expected = 0.299 * 100 + 0.587 * 150 + 0.114 * 200;
|
||||
assertEquals(expected, customLuminance, 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToMonochrome() {
|
||||
// Test pure black -> should stay black
|
||||
Color result = CalcUtils.convertToMonochrome(Color.BLACK);
|
||||
assertEquals(Color.BLACK, result);
|
||||
|
||||
// Test pure white -> should stay white
|
||||
result = CalcUtils.convertToMonochrome(Color.WHITE);
|
||||
assertEquals(Color.WHITE, result);
|
||||
|
||||
// Test dark color -> should become black
|
||||
Color darkBlue = new Color(0, 0, 50);
|
||||
result = CalcUtils.convertToMonochrome(darkBlue);
|
||||
assertEquals(Color.BLACK, result);
|
||||
|
||||
// Test light color -> should become white
|
||||
Color lightYellow = new Color(255, 255, 200);
|
||||
result = CalcUtils.convertToMonochrome(lightYellow);
|
||||
assertEquals(Color.WHITE, result);
|
||||
|
||||
// Test middle gray -> should become white (threshold is 128, gray is exactly 128)
|
||||
Color gray = new Color(128, 128, 128);
|
||||
result = CalcUtils.convertToMonochrome(gray);
|
||||
assertEquals(Color.WHITE, result);
|
||||
|
||||
// Test color just below threshold -> should become black
|
||||
Color darkGray = new Color(127, 127, 127);
|
||||
result = CalcUtils.convertToMonochrome(darkGray);
|
||||
assertEquals(Color.BLACK, result);
|
||||
|
||||
// Test color just above threshold -> should become white
|
||||
Color lightGray = new Color(129, 129, 129);
|
||||
result = CalcUtils.convertToMonochrome(lightGray);
|
||||
assertEquals(Color.WHITE, result);
|
||||
|
||||
// Test with custom threshold
|
||||
Color mediumColor = new Color(100, 100, 100);
|
||||
result = CalcUtils.convertToMonochrome(mediumColor, 50.0);
|
||||
assertEquals(Color.WHITE, result); // Luminance ~100, threshold 50, so white
|
||||
|
||||
result = CalcUtils.convertToMonochrome(mediumColor, 150.0);
|
||||
assertEquals(Color.BLACK, result); // Luminance ~100, threshold 150, so black
|
||||
|
||||
// Test null color
|
||||
assertNull(CalcUtils.convertToMonochrome(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToMonochromeWithAlpha() {
|
||||
// Test color with alpha channel - alpha should be preserved
|
||||
// Red (255, 0, 0) has luminance ~76, which is < 128, so it becomes BLACK
|
||||
Color transparentRed = new Color(255, 0, 0, 128);
|
||||
Color result = CalcUtils.convertToMonochrome(transparentRed);
|
||||
assertEquals(Color.BLACK.getRed(), result.getRed());
|
||||
assertEquals(Color.BLACK.getGreen(), result.getGreen());
|
||||
assertEquals(Color.BLACK.getBlue(), result.getBlue());
|
||||
assertEquals(128, result.getAlpha());
|
||||
|
||||
// Test light color with alpha - should become WHITE
|
||||
Color transparentLight = new Color(255, 255, 200, 128);
|
||||
result = CalcUtils.convertToMonochrome(transparentLight);
|
||||
assertEquals(Color.WHITE.getRed(), result.getRed());
|
||||
assertEquals(Color.WHITE.getGreen(), result.getGreen());
|
||||
assertEquals(Color.WHITE.getBlue(), result.getBlue());
|
||||
assertEquals(128, result.getAlpha());
|
||||
|
||||
// Test fully transparent color
|
||||
Color transparent = new Color(100, 150, 200, 0);
|
||||
result = CalcUtils.convertToMonochrome(transparent);
|
||||
assertEquals(0, result.getAlpha());
|
||||
|
||||
// Test opaque color - alpha should remain 255
|
||||
Color opaque = new Color(50, 50, 50);
|
||||
result = CalcUtils.convertToMonochrome(opaque);
|
||||
assertEquals(255, result.getAlpha());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
package org.diylc.presenter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.AlphaComposite;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
|
||||
import org.diylc.appframework.simplemq.MessageDispatcher;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.common.EventType;
|
||||
import org.diylc.core.BoardUndersideDisplay;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IBoard;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.testcomponents.MockDIYComponent;
|
||||
import org.junit.Test;
|
||||
|
||||
public class DrawingManagerTest {
|
||||
|
||||
@ComponentDescriptor(name = "MockBoard", author = "test", category = "Boards", instanceNamePrefix = "B",
|
||||
description = "Mock board for testing", zOrder = IDIYComponent.BOARD)
|
||||
public static class MockBoard extends MockDIYComponent implements IBoard {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Rectangle2D boardRectangle = new Rectangle2D.Double(100, 100, 200, 300);
|
||||
private BoardUndersideDisplay undersideDisplay = BoardUndersideDisplay.BELOW;
|
||||
private Size undersideOffset = new Size(20.0, SizeUnit.px);
|
||||
private Boolean undersideTransparency = Boolean.TRUE;
|
||||
|
||||
public List<AffineTransform> capturedTransforms = new ArrayList<>();
|
||||
public List<Float> capturedAlphas = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Rectangle2D getBoardRectangle() {
|
||||
return boardRectangle;
|
||||
}
|
||||
|
||||
public void setBoardRectangle(Rectangle2D boardRectangle) {
|
||||
this.boardRectangle = boardRectangle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoardUndersideDisplay getUndersideDisplay() {
|
||||
return undersideDisplay;
|
||||
}
|
||||
|
||||
public void setUndersideDisplay(BoardUndersideDisplay undersideDisplay) {
|
||||
this.undersideDisplay = undersideDisplay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Size getUndersideOffset() {
|
||||
return undersideOffset;
|
||||
}
|
||||
|
||||
public void setUndersideOffset(Size undersideOffset) {
|
||||
this.undersideOffset = undersideOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getUndersideTransparency() {
|
||||
return undersideTransparency;
|
||||
}
|
||||
|
||||
public void setUndersideTransparency(Boolean undersideTransparency) {
|
||||
this.undersideTransparency = undersideTransparency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldExportToGerber() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
capturedTransforms.add(new AffineTransform(g2d.getTransform()));
|
||||
if (g2d.getComposite() instanceof AlphaComposite) {
|
||||
capturedAlphas.add(((AlphaComposite) g2d.getComposite()).getAlpha());
|
||||
} else {
|
||||
capturedAlphas.add(1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TransformTrackingComponent extends MockDIYComponent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public List<AffineTransform> capturedTransforms = new ArrayList<>();
|
||||
public List<Float> capturedAlphas = new ArrayList<>();
|
||||
private Point2D controlPoint = new Point2D.Double(150, 150);
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return controlPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
capturedTransforms.add(new AffineTransform(g2d.getTransform()));
|
||||
if (g2d.getComposite() instanceof AlphaComposite) {
|
||||
capturedAlphas.add(((AlphaComposite) g2d.getComposite()).getAlpha());
|
||||
} else {
|
||||
capturedAlphas.add(1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DrawingManager createDrawingManager() {
|
||||
return new DrawingManager(new MessageDispatcher<EventType>(false), InMemoryConfigurationManager.getInstance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMirrorBelow() {
|
||||
Project project = new Project();
|
||||
MockBoard board = new MockBoard();
|
||||
board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300));
|
||||
board.setUndersideDisplay(BoardUndersideDisplay.BELOW);
|
||||
board.setUndersideOffset(new Size(20.0, SizeUnit.px));
|
||||
board.setUndersideTransparency(true);
|
||||
|
||||
TransformTrackingComponent component = new TransformTrackingComponent();
|
||||
project.getComponents().add(board);
|
||||
project.getComponents().add(component);
|
||||
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
// The component should be drawn twice: once normally and once mirrored
|
||||
assertEquals(2, component.capturedTransforms.size());
|
||||
|
||||
// Original drawing: identity / no reflection
|
||||
AffineTransform normalTx = component.capturedTransforms.get(0);
|
||||
Point2D p = new Point2D.Double(150, 150);
|
||||
Point2D normalP = normalTx.transform(p, null);
|
||||
assertEquals(150.0, normalP.getX(), 0.001);
|
||||
assertEquals(150.0, normalP.getY(), 0.001);
|
||||
|
||||
// Mirrored drawing: BELOW reflection across Y = maxY + offset/2 = 400 + 10 = 410
|
||||
// Expected y' = 2 * 410 - 150 = 820 - 150 = 670
|
||||
AffineTransform mirrorTx = component.capturedTransforms.get(1);
|
||||
Point2D mirroredP = mirrorTx.transform(p, null);
|
||||
assertEquals(150.0, mirroredP.getX(), 0.001);
|
||||
assertEquals(670.0, mirroredP.getY(), 0.001);
|
||||
|
||||
// Check transparency
|
||||
assertEquals(2, component.capturedAlphas.size());
|
||||
assertEquals(1.0f, component.capturedAlphas.get(0), 0.01f);
|
||||
assertEquals(0.5f, component.capturedAlphas.get(1), 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMirrorAbove() {
|
||||
Project project = new Project();
|
||||
MockBoard board = new MockBoard();
|
||||
board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300));
|
||||
board.setUndersideDisplay(BoardUndersideDisplay.ABOVE);
|
||||
board.setUndersideOffset(new Size(40.0, SizeUnit.px));
|
||||
board.setUndersideTransparency(false);
|
||||
|
||||
TransformTrackingComponent component = new TransformTrackingComponent();
|
||||
project.getComponents().add(board);
|
||||
project.getComponents().add(component);
|
||||
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
assertEquals(2, component.capturedTransforms.size());
|
||||
|
||||
// Mirrored drawing: ABOVE reflection across Y = minY - offset/2 = 100 - 20 = 80
|
||||
// Expected y' = 2 * 80 - 150 = 160 - 150 = 10
|
||||
AffineTransform mirrorTx = component.capturedTransforms.get(1);
|
||||
Point2D mirroredP = mirrorTx.transform(new Point2D.Double(150, 150), null);
|
||||
assertEquals(150.0, mirroredP.getX(), 0.001);
|
||||
assertEquals(10.0, mirroredP.getY(), 0.001);
|
||||
|
||||
// Transparency should not be 0.5 when undersideTransparency is false
|
||||
assertEquals(1.0f, component.capturedAlphas.get(1), 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMirrorLeft() {
|
||||
Project project = new Project();
|
||||
MockBoard board = new MockBoard();
|
||||
board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300));
|
||||
board.setUndersideDisplay(BoardUndersideDisplay.LEFT);
|
||||
board.setUndersideOffset(new Size(20.0, SizeUnit.px));
|
||||
|
||||
TransformTrackingComponent component = new TransformTrackingComponent();
|
||||
project.getComponents().add(board);
|
||||
project.getComponents().add(component);
|
||||
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
assertEquals(2, component.capturedTransforms.size());
|
||||
|
||||
// Mirrored drawing: LEFT reflection across X = minX - offset/2 = 100 - 10 = 90
|
||||
// Expected x' = 2 * 90 - 150 = 180 - 150 = 30
|
||||
AffineTransform mirrorTx = component.capturedTransforms.get(1);
|
||||
Point2D mirroredP = mirrorTx.transform(new Point2D.Double(150, 150), null);
|
||||
assertEquals(30.0, mirroredP.getX(), 0.001);
|
||||
assertEquals(150.0, mirroredP.getY(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMirrorRight() {
|
||||
Project project = new Project();
|
||||
MockBoard board = new MockBoard();
|
||||
board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300));
|
||||
board.setUndersideDisplay(BoardUndersideDisplay.RIGHT);
|
||||
board.setUndersideOffset(new Size(20.0, SizeUnit.px));
|
||||
|
||||
TransformTrackingComponent component = new TransformTrackingComponent();
|
||||
project.getComponents().add(board);
|
||||
project.getComponents().add(component);
|
||||
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
assertEquals(2, component.capturedTransforms.size());
|
||||
|
||||
// Mirrored drawing: RIGHT reflection across X = maxX + offset/2 = 300 + 10 = 310
|
||||
// Expected x' = 2 * 310 - 150 = 620 - 150 = 470
|
||||
AffineTransform mirrorTx = component.capturedTransforms.get(1);
|
||||
Point2D mirroredP = mirrorTx.transform(new Point2D.Double(150, 150), null);
|
||||
assertEquals(470.0, mirroredP.getX(), 0.001);
|
||||
assertEquals(150.0, mirroredP.getY(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUndersideDisplayNone() {
|
||||
Project project = new Project();
|
||||
MockBoard board = new MockBoard();
|
||||
board.setUndersideDisplay(BoardUndersideDisplay.NONE);
|
||||
|
||||
TransformTrackingComponent component = new TransformTrackingComponent();
|
||||
project.getComponents().add(board);
|
||||
project.getComponents().add(component);
|
||||
|
||||
DrawingManager drawingManager = createDrawingManager();
|
||||
BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = img.createGraphics();
|
||||
|
||||
drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
|
||||
Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
// No mirrored drawing
|
||||
assertEquals(1, component.capturedTransforms.size());
|
||||
assertEquals(1, board.capturedTransforms.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTextUnmirroringHorizontal() {
|
||||
BufferedImage img = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
|
||||
final List<AffineTransform> capturedDuringDrawString = new ArrayList<>();
|
||||
Graphics2D canvasG2d = new Graphics2DProxy(img.createGraphics()) {
|
||||
@Override
|
||||
public void drawString(String str, int x, int y) {
|
||||
capturedDuringDrawString.add(new AffineTransform(getTransform()));
|
||||
super.drawString(str, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawString(String str, float x, float y) {
|
||||
capturedDuringDrawString.add(new AffineTransform(getTransform()));
|
||||
super.drawString(str, x, y);
|
||||
}
|
||||
};
|
||||
|
||||
G2DWrapper wrapper = new G2DWrapper(canvasG2d, 1.0, EnumSet.noneOf(DrawOption.class));
|
||||
|
||||
// Simulate horizontal mirror mode
|
||||
wrapper.setMirrorDirection(G2DWrapper.MirrorDirection.HORIZONTAL);
|
||||
AffineTransform mirrorTx = new AffineTransform();
|
||||
mirrorTx.translate(300, 0);
|
||||
mirrorTx.scale(-1.0, 1.0);
|
||||
mirrorTx.translate(-300, 0);
|
||||
wrapper.transform(mirrorTx);
|
||||
|
||||
wrapper.drawString("10K Log", 200, 200);
|
||||
|
||||
assertEquals(1, capturedDuringDrawString.size());
|
||||
AffineTransform textTx = capturedDuringDrawString.get(0);
|
||||
|
||||
// The determinant should be positive (un-mirrored / right-reading text)
|
||||
assertTrue("Text transform determinant should be positive (not mirrored)", textTx.getDeterminant() > 0);
|
||||
|
||||
// After drawing, wrapper transform should still be the mirrorTx
|
||||
assertEquals(-1.0, wrapper.getTransform().getScaleX(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTextUnmirroringVertical() {
|
||||
BufferedImage img = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
|
||||
final List<AffineTransform> capturedDuringDrawString = new ArrayList<>();
|
||||
Graphics2D canvasG2d = new Graphics2DProxy(img.createGraphics()) {
|
||||
@Override
|
||||
public void drawString(String str, int x, int y) {
|
||||
capturedDuringDrawString.add(new AffineTransform(getTransform()));
|
||||
super.drawString(str, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawString(String str, float x, float y) {
|
||||
capturedDuringDrawString.add(new AffineTransform(getTransform()));
|
||||
super.drawString(str, x, y);
|
||||
}
|
||||
};
|
||||
|
||||
G2DWrapper wrapper = new G2DWrapper(canvasG2d, 1.0, EnumSet.noneOf(DrawOption.class));
|
||||
|
||||
// Simulate vertical mirror mode
|
||||
wrapper.setMirrorDirection(G2DWrapper.MirrorDirection.VERTICAL);
|
||||
AffineTransform mirrorTx = new AffineTransform();
|
||||
mirrorTx.translate(0, 300);
|
||||
mirrorTx.scale(1.0, -1.0);
|
||||
mirrorTx.translate(0, -300);
|
||||
wrapper.transform(mirrorTx);
|
||||
|
||||
wrapper.drawString("47uF", 200, 200);
|
||||
|
||||
assertEquals(1, capturedDuringDrawString.size());
|
||||
AffineTransform textTx = capturedDuringDrawString.get(0);
|
||||
|
||||
// The determinant should be positive (un-mirrored / right-reading text)
|
||||
assertTrue("Text transform determinant should be positive (not mirrored)", textTx.getDeterminant() > 0);
|
||||
|
||||
// After drawing, wrapper transform should still be the mirrorTx
|
||||
assertEquals(-1.0, wrapper.getTransform().getScaleY(), 0.001);
|
||||
}
|
||||
|
||||
// Simple delegating Graphics2D proxy for testing
|
||||
private static class Graphics2DProxy extends Graphics2D {
|
||||
private final Graphics2D delegate;
|
||||
|
||||
public Graphics2DProxy(Graphics2D delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(java.awt.Shape s) { delegate.draw(s); }
|
||||
@Override
|
||||
public boolean drawImage(java.awt.Image img, AffineTransform xform, java.awt.image.ImageObserver obs) { return delegate.drawImage(img, xform, obs); }
|
||||
@Override
|
||||
public void drawImage(BufferedImage img, java.awt.image.BufferedImageOp op, int x, int y) { delegate.drawImage(img, op, x, y); }
|
||||
@Override
|
||||
public void drawRenderedImage(java.awt.image.RenderedImage img, AffineTransform xform) { delegate.drawRenderedImage(img, xform); }
|
||||
@Override
|
||||
public void drawRenderableImage(java.awt.image.renderable.RenderableImage img, AffineTransform xform) { delegate.drawRenderableImage(img, xform); }
|
||||
@Override
|
||||
public void drawString(String str, int x, int y) { delegate.drawString(str, x, y); }
|
||||
@Override
|
||||
public void drawString(String str, float x, float y) { delegate.drawString(str, x, y); }
|
||||
@Override
|
||||
public void drawString(java.text.AttributedCharacterIterator iterator, int x, int y) { delegate.drawString(iterator, x, y); }
|
||||
@Override
|
||||
public void drawString(java.text.AttributedCharacterIterator iterator, float x, float y) { delegate.drawString(iterator, x, y); }
|
||||
@Override
|
||||
public void drawGlyphVector(java.awt.font.GlyphVector g, float x, float y) { delegate.drawGlyphVector(g, x, y); }
|
||||
@Override
|
||||
public void fill(java.awt.Shape s) { delegate.fill(s); }
|
||||
@Override
|
||||
public boolean hit(java.awt.Rectangle rect, java.awt.Shape s, boolean onStroke) { return delegate.hit(rect, s, onStroke); }
|
||||
@Override
|
||||
public java.awt.GraphicsConfiguration getDeviceConfiguration() { return delegate.getDeviceConfiguration(); }
|
||||
@Override
|
||||
public void setComposite(java.awt.Composite comp) { delegate.setComposite(comp); }
|
||||
@Override
|
||||
public void setPaint(java.awt.Paint paint) { delegate.setPaint(paint); }
|
||||
@Override
|
||||
public void setStroke(java.awt.Stroke s) { delegate.setStroke(s); }
|
||||
@Override
|
||||
public void setRenderingHint(java.awt.RenderingHints.Key hintKey, Object hintValue) { delegate.setRenderingHint(hintKey, hintValue); }
|
||||
@Override
|
||||
public Object getRenderingHint(java.awt.RenderingHints.Key hintKey) { return delegate.getRenderingHint(hintKey); }
|
||||
@Override
|
||||
public void setRenderingHints(java.util.Map<?, ?> hints) { delegate.setRenderingHints(hints); }
|
||||
@Override
|
||||
public void addRenderingHints(java.util.Map<?, ?> hints) { delegate.addRenderingHints(hints); }
|
||||
@Override
|
||||
public java.awt.RenderingHints getRenderingHints() { return delegate.getRenderingHints(); }
|
||||
@Override
|
||||
public void translate(int x, int y) { delegate.translate(x, y); }
|
||||
@Override
|
||||
public void translate(double tx, double ty) { delegate.translate(tx, ty); }
|
||||
@Override
|
||||
public void rotate(double theta) { delegate.rotate(theta); }
|
||||
@Override
|
||||
public void rotate(double theta, double x, double y) { delegate.rotate(theta, x, y); }
|
||||
@Override
|
||||
public void scale(double sx, double sy) { delegate.scale(sx, sy); }
|
||||
@Override
|
||||
public void shear(double shx, double shy) { delegate.shear(shx, shy); }
|
||||
@Override
|
||||
public void transform(AffineTransform Tx) { delegate.transform(Tx); }
|
||||
@Override
|
||||
public void setTransform(AffineTransform Tx) { delegate.setTransform(Tx); }
|
||||
@Override
|
||||
public AffineTransform getTransform() { return delegate.getTransform(); }
|
||||
@Override
|
||||
public java.awt.Paint getPaint() { return delegate.getPaint(); }
|
||||
@Override
|
||||
public java.awt.Composite getComposite() { return delegate.getComposite(); }
|
||||
@Override
|
||||
public void setBackground(java.awt.Color color) { delegate.setBackground(color); }
|
||||
@Override
|
||||
public java.awt.Color getBackground() { return delegate.getBackground(); }
|
||||
@Override
|
||||
public java.awt.Stroke getStroke() { return delegate.getStroke(); }
|
||||
@Override
|
||||
public void clip(java.awt.Shape s) { delegate.clip(s); }
|
||||
@Override
|
||||
public java.awt.font.FontRenderContext getFontRenderContext() { return delegate.getFontRenderContext(); }
|
||||
@Override
|
||||
public java.awt.Graphics create() { return delegate.create(); }
|
||||
@Override
|
||||
public java.awt.Color getColor() { return delegate.getColor(); }
|
||||
@Override
|
||||
public void setColor(java.awt.Color c) { delegate.setColor(c); }
|
||||
@Override
|
||||
public void setPaintMode() { delegate.setPaintMode(); }
|
||||
@Override
|
||||
public void setXORMode(java.awt.Color c1) { delegate.setXORMode(c1); }
|
||||
@Override
|
||||
public java.awt.Font getFont() { return delegate.getFont(); }
|
||||
@Override
|
||||
public void setFont(java.awt.Font font) { delegate.setFont(font); }
|
||||
@Override
|
||||
public java.awt.FontMetrics getFontMetrics(java.awt.Font f) { return delegate.getFontMetrics(f); }
|
||||
@Override
|
||||
public java.awt.Rectangle getClipBounds() { return delegate.getClipBounds(); }
|
||||
@Override
|
||||
public void clipRect(int x, int y, int width, int height) { delegate.clipRect(x, y, width, height); }
|
||||
@Override
|
||||
public void setClip(int x, int y, int width, int height) { delegate.setClip(x, y, width, height); }
|
||||
@Override
|
||||
public java.awt.Shape getClip() { return delegate.getClip(); }
|
||||
@Override
|
||||
public void setClip(java.awt.Shape clip) { delegate.setClip(clip); }
|
||||
@Override
|
||||
public void copyArea(int x, int y, int width, int height, int dx, int dy) { delegate.copyArea(x, y, width, height, dx, dy); }
|
||||
@Override
|
||||
public void drawLine(int x1, int y1, int x2, int y2) { delegate.drawLine(x1, y1, x2, y2); }
|
||||
@Override
|
||||
public void fillRect(int x, int y, int width, int height) { delegate.fillRect(x, y, width, height); }
|
||||
@Override
|
||||
public void clearRect(int x, int y, int width, int height) { delegate.clearRect(x, y, width, height); }
|
||||
@Override
|
||||
public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { delegate.drawRoundRect(x, y, width, height, arcWidth, arcHeight); }
|
||||
@Override
|
||||
public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { delegate.fillRoundRect(x, y, width, height, arcWidth, arcHeight); }
|
||||
@Override
|
||||
public void drawOval(int x, int y, int width, int height) { delegate.drawOval(x, y, width, height); }
|
||||
@Override
|
||||
public void fillOval(int x, int y, int width, int height) { delegate.fillOval(x, y, width, height); }
|
||||
@Override
|
||||
public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { delegate.drawArc(x, y, width, height, startAngle, arcAngle); }
|
||||
@Override
|
||||
public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { delegate.fillArc(x, y, width, height, startAngle, arcAngle); }
|
||||
@Override
|
||||
public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { delegate.drawPolyline(xPoints, yPoints, nPoints); }
|
||||
@Override
|
||||
public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { delegate.drawPolygon(xPoints, yPoints, nPoints); }
|
||||
@Override
|
||||
public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { delegate.fillPolygon(xPoints, yPoints, nPoints); }
|
||||
@Override
|
||||
public boolean drawImage(java.awt.Image img, int x, int y, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, x, y, observer); }
|
||||
@Override
|
||||
public boolean drawImage(java.awt.Image img, int x, int y, int width, int height, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, x, y, width, height, observer); }
|
||||
@Override
|
||||
public boolean drawImage(java.awt.Image img, int x, int y, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, x, y, bgcolor, observer); }
|
||||
@Override
|
||||
public boolean drawImage(java.awt.Image img, int x, int y, int width, int height, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, x, y, width, height, bgcolor, observer); }
|
||||
@Override
|
||||
public boolean drawImage(java.awt.Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); }
|
||||
@Override
|
||||
public boolean drawImage(java.awt.Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); }
|
||||
@Override
|
||||
public void dispose() { delegate.dispose(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,173 +1,173 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.testcomponents;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.components.AbstractComponent;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.core.measures.Capacitance;
|
||||
import org.diylc.core.measures.CapacitanceUnit;
|
||||
import org.diylc.core.measures.Resistance;
|
||||
import org.diylc.core.measures.ResistanceUnit;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
@ComponentDescriptor(name = "Mock", author = "bancika", category = "Sample", instanceNamePrefix = "M",
|
||||
description = "test", zOrder = IDIYComponent.COMPONENT)
|
||||
public class MockDIYComponent extends AbstractComponent<Capacitance> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String testField = "Hello World";
|
||||
private Capacitance c = new Capacitance(100d, CapacitanceUnit.uF);
|
||||
private Resistance r = new Resistance(123d, ResistanceUnit.K);
|
||||
private Size s = new Size(1d, SizeUnit.cm);
|
||||
private Color color = Color.green;
|
||||
private Point2D leftTopCorner = new Point(0, 0);
|
||||
private String name = "something";
|
||||
|
||||
public MockDIYComponent(String testField, Capacitance c, Resistance r, Size s, Color color, Point leftTopCorner) {
|
||||
super();
|
||||
this.testField = testField;
|
||||
this.c = c;
|
||||
this.r = r;
|
||||
this.s = s;
|
||||
this.color = color;
|
||||
this.leftTopCorner = leftTopCorner;
|
||||
}
|
||||
|
||||
public MockDIYComponent() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canControlPointOverlap(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@EditableProperty
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.WHEN_SELECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return leftTopCorner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
leftTopCorner.setLocation(point);
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Test Field")
|
||||
public String getTestField() {
|
||||
return testField;
|
||||
}
|
||||
|
||||
public void setTestField(String testField) {
|
||||
this.testField = testField;
|
||||
}
|
||||
|
||||
@EditableProperty(defaultable = true)
|
||||
public Capacitance getValue() {
|
||||
return c;
|
||||
}
|
||||
|
||||
public void setValue(Capacitance c) {
|
||||
this.c = c;
|
||||
}
|
||||
|
||||
@EditableProperty(defaultable = true)
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(Color color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
@EditableProperty
|
||||
public Resistance getR() {
|
||||
return r;
|
||||
}
|
||||
|
||||
// Setter is commented out, ClassProcessor should ignore this property.
|
||||
// public void setR(Resistance r) {
|
||||
// this.r = r;
|
||||
// }
|
||||
|
||||
// No annotation, so this property should be ignored.
|
||||
public Size getS() {
|
||||
return s;
|
||||
}
|
||||
|
||||
public void setS(Size s) {
|
||||
this.s = s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
g2d.setColor(componentState.equals(ComponentState.SELECTED) ? color : color.darker());
|
||||
g2d.fillRect((int)leftTopCorner.getX(), (int)leftTopCorner.getY(), 200, 50);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
g2d.drawString("X", 10, 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.testcomponents;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.components.AbstractComponent;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.core.measures.Capacitance;
|
||||
import org.diylc.core.measures.CapacitanceUnit;
|
||||
import org.diylc.core.measures.Resistance;
|
||||
import org.diylc.core.measures.ResistanceUnit;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
@ComponentDescriptor(name = "Mock", author = "bancika", category = "Sample", instanceNamePrefix = "M",
|
||||
description = "test", zOrder = IDIYComponent.COMPONENT)
|
||||
public class MockDIYComponent extends AbstractComponent<Capacitance> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String testField = "Hello World";
|
||||
private Capacitance c = new Capacitance(100d, CapacitanceUnit.uF);
|
||||
private Resistance r = new Resistance(123d, ResistanceUnit.K);
|
||||
private Size s = new Size(1d, SizeUnit.cm);
|
||||
private Color color = Color.green;
|
||||
private Point2D leftTopCorner = new Point(0, 0);
|
||||
private String name = "something";
|
||||
|
||||
public MockDIYComponent(String testField, Capacitance c, Resistance r, Size s, Color color, Point leftTopCorner) {
|
||||
super();
|
||||
this.testField = testField;
|
||||
this.c = c;
|
||||
this.r = r;
|
||||
this.s = s;
|
||||
this.color = color;
|
||||
this.leftTopCorner = leftTopCorner;
|
||||
}
|
||||
|
||||
public MockDIYComponent() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canControlPointOverlap(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@EditableProperty
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.WHEN_SELECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return leftTopCorner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
leftTopCorner.setLocation(point);
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Test Field")
|
||||
public String getTestField() {
|
||||
return testField;
|
||||
}
|
||||
|
||||
public void setTestField(String testField) {
|
||||
this.testField = testField;
|
||||
}
|
||||
|
||||
@EditableProperty(defaultable = true)
|
||||
public Capacitance getValue() {
|
||||
return c;
|
||||
}
|
||||
|
||||
public void setValue(Capacitance c) {
|
||||
this.c = c;
|
||||
}
|
||||
|
||||
@EditableProperty(defaultable = true)
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(Color color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
@EditableProperty
|
||||
public Resistance getR() {
|
||||
return r;
|
||||
}
|
||||
|
||||
// Setter is commented out, ClassProcessor should ignore this property.
|
||||
// public void setR(Resistance r) {
|
||||
// this.r = r;
|
||||
// }
|
||||
|
||||
// No annotation, so this property should be ignored.
|
||||
public Size getS() {
|
||||
return s;
|
||||
}
|
||||
|
||||
public void setS(Size s) {
|
||||
this.s = s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
g2d.setColor(componentState.equals(ComponentState.SELECTED) ? color : color.darker());
|
||||
g2d.fillRect((int) Math.round(leftTopCorner.getX()), (int) Math.round(leftTopCorner.getY()), 200, 50);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
g2d.drawString("X", 10, 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.testcomponents;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.components.AbstractComponent;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
|
||||
/**
|
||||
* Minimal single-point, sticky component with a settable node name, standing in for a solder pad
|
||||
* or turret when testing how {@code CompositeComponent} resolves the terminal names of a custom
|
||||
* board. A {@code null} node name marks the point as not being a netlist node, like a pad whose
|
||||
* Node Name is left blank.
|
||||
*/
|
||||
@ComponentDescriptor(name = "One Point Test", category = "Test", author = "Test",
|
||||
description = "test", instanceNamePrefix = "OP", zOrder = IDIYComponent.COMPONENT)
|
||||
public class OnePointTestComponent extends AbstractComponent<Void> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Point2D point;
|
||||
private String nodeName;
|
||||
|
||||
public OnePointTestComponent() {
|
||||
this("", null, new Point2D.Double(0, 0));
|
||||
}
|
||||
|
||||
public OnePointTestComponent(String name, String nodeName, Point2D point) {
|
||||
setName(name);
|
||||
this.nodeName = nodeName;
|
||||
this.point = point;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return point;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
this.point = point;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.ALWAYS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
|
||||
Project project, IDrawingObserver drawingObserver) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.testcomponents;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.core.ISwitch;
|
||||
|
||||
/**
|
||||
* Smallest possible {@link ISwitch} implementation, used to verify that a building block
|
||||
* containing a switch can still be instantiated as a {@code CompositeComponent} (design decision
|
||||
* D6 in {@code docs/plans/composite-building-blocks.md} - the composite itself is not
|
||||
* {@code ISwitch}, so the switch places and draws but contributes no switching behavior).
|
||||
* <p>
|
||||
* Deliberately a public top-level class, not a nested one: {@link
|
||||
* org.diylc.components.AbstractComponent#clone()} instantiates the no-arg constructor via plain
|
||||
* reflection (no {@code setAccessible}), which fails across packages unless both the class and
|
||||
* the constructor are public.
|
||||
*/
|
||||
public class SwitchTestComponent extends TwoPointTestComponent implements ISwitch {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public SwitchTestComponent() {
|
||||
this("SW");
|
||||
}
|
||||
|
||||
public SwitchTestComponent(String name) {
|
||||
super(name, new Point2D.Double(0, 0), new Point2D.Double(10, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPositionCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPositionName(int position) {
|
||||
return position == 0 ? "ON" : "OFF";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean arePointsConnected(int index1, int index2, int position) {
|
||||
return position == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.testcomponents;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
|
||||
import org.diylc.components.AbstractComponent;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
|
||||
/**
|
||||
* Minimal two-point component used to exercise {@code CompositeComponent} and
|
||||
* {@code BuildingBlockManager} in tests: two control points, individually configurable
|
||||
* stickiness, node naming left at the {@link AbstractComponent} default (the 1-based point
|
||||
* index), so a composite's per-child node name delegation can be verified precisely.
|
||||
*/
|
||||
@ComponentDescriptor(name = "Two Point Test", category = "Test", author = "Test",
|
||||
description = "test", instanceNamePrefix = "TP", zOrder = IDIYComponent.COMPONENT)
|
||||
public class TwoPointTestComponent extends AbstractComponent<Void> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Point2D p0;
|
||||
private Point2D p1;
|
||||
private boolean sticky0 = true;
|
||||
private boolean sticky1 = true;
|
||||
|
||||
public TwoPointTestComponent() {
|
||||
this("", new Point2D.Double(0, 0), new Point2D.Double(10, 0));
|
||||
}
|
||||
|
||||
public TwoPointTestComponent(String name, Point2D p0, Point2D p1) {
|
||||
setName(name);
|
||||
this.p0 = p0;
|
||||
this.p1 = p1;
|
||||
}
|
||||
|
||||
public void setSticky(int index, boolean sticky) {
|
||||
if (index == 0)
|
||||
sticky0 = sticky;
|
||||
else
|
||||
sticky1 = sticky;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return index == 0 ? p0 : p1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
if (index == 0)
|
||||
p0 = point;
|
||||
else
|
||||
p1 = point;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return index == 0 ? sticky0 : sticky1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.ALWAYS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
member-accessor-reflection
|
||||
@@ -0,0 +1 @@
|
||||
mock-maker-subclass
|
||||
@@ -7,15 +7,15 @@
|
||||
<parent>
|
||||
<groupId>org.diylc</groupId>
|
||||
<artifactId>diylc-parent</artifactId>
|
||||
<version>5.7.0</version>
|
||||
<version>6.7.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>diylc-library</artifactId>
|
||||
<!-- No need to specify version - it inherits from parent -->
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -31,7 +31,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.diylc.common.HorizontalAlignment;
|
||||
import org.diylc.common.VerticalAlignment;
|
||||
@@ -72,13 +71,18 @@ public class StringUtils {
|
||||
* @return a non-empty list of strings
|
||||
*/
|
||||
public static List<String> wrap(String str, FontMetrics fm, int maxWidth) {
|
||||
String[] split = str.split("\\<br\\>");
|
||||
List<String> lines = Arrays.stream(split).flatMap(s -> splitIntoLines(s).stream()).collect(Collectors.toList());
|
||||
if (lines.size() == 0)
|
||||
return lines;
|
||||
// First split by actual newlines to honor them, then split by <br> tags
|
||||
List<String> lines = splitIntoLines(str);
|
||||
List<String> allLines = new ArrayList<String>();
|
||||
for (String line : lines) {
|
||||
String[] split = line.split("\\<br\\>");
|
||||
allLines.addAll(Arrays.asList(split));
|
||||
}
|
||||
if (allLines.size() == 0)
|
||||
return allLines;
|
||||
|
||||
ArrayList<String> strings = new ArrayList<String>();
|
||||
for (Iterator<String> iter = lines.iterator(); iter.hasNext();)
|
||||
for (Iterator<String> iter = allLines.iterator(); iter.hasNext();)
|
||||
wrapLineInto((String) iter.next(), strings, fm, maxWidth);
|
||||
return strings;
|
||||
}
|
||||
@@ -126,8 +130,8 @@ public class StringUtils {
|
||||
line = line.substring(pos).trim();
|
||||
len = line.length();
|
||||
}
|
||||
if (len > 0)
|
||||
list.add(line);
|
||||
// Always add the line, even if empty, to preserve empty lines from newlines
|
||||
list.add(line);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,12 +213,24 @@ public class StringUtils {
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
String[] parts = text.split("\\<br\\>");
|
||||
// Use -1 limit to preserve trailing empty strings so we can filter them out
|
||||
String[] parts = text.split("\\<br\\>", -1);
|
||||
if (parts.length > 1) {
|
||||
// Remove trailing empty strings to avoid rendering extra blank lines
|
||||
int lastNonEmpty = parts.length - 1;
|
||||
while (lastNonEmpty >= 0 && parts[lastNonEmpty].trim().isEmpty()) {
|
||||
lastNonEmpty--;
|
||||
}
|
||||
// If all parts are empty, don't render anything
|
||||
if (lastNonEmpty < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FontMetrics fontMetrics = g2d.getFontMetrics();
|
||||
Rectangle stringBounds = fontMetrics.getStringBounds(parts[0], g2d).getBounds();
|
||||
for (int i = 0; i < parts.length; i++)
|
||||
drawCenteredText(g2d, parts[i], x, (int)(y - stringBounds.height * (parts.length - 1) / 2d + i * stringBounds.height), horizontalAlignment, verticalAlignment);
|
||||
int actualLength = lastNonEmpty + 1;
|
||||
for (int i = 0; i < actualLength; i++)
|
||||
drawCenteredText(g2d, parts[i], x, (int)(y - stringBounds.height * (actualLength - 1) / 2d + i * stringBounds.height), horizontalAlignment, verticalAlignment);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
|
||||
DIY Layout Creator (DIYLC).
|
||||
Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
|
||||
This file is part of DIYLC.
|
||||
|
||||
DIYLC is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
DIYLC is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
package org.diylc.components;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
|
||||
/**
|
||||
* Base for the boards built out of addressable 5050 LEDs -- the rings, the jewel, the stick, the
|
||||
* strip, the matrix and the bare breakout.
|
||||
*
|
||||
* <p>Every one of them is sold fitted with any of the packages {@link LedType} lists, on the same
|
||||
* PCB with the same footprint, so the package is a property of the board rather than a board of
|
||||
* its own. What it changes is what the LEDs can emit, which is the palette here, and what the BOM
|
||||
* has to ask for, which each board folds into its own variant label.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public abstract class AbstractAddressableLedBoard extends AbstractMakerBoard {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected LedType ledType = LedType.RGB;
|
||||
|
||||
// Rebuilt only when the count or the package changes, because draw() runs on every repaint.
|
||||
private transient Color[] ledColors;
|
||||
|
||||
@EditableProperty(name = "LED Type")
|
||||
public LedType getLedType() {
|
||||
if (ledType == null) {
|
||||
ledType = LedType.RGB;
|
||||
}
|
||||
return ledType;
|
||||
}
|
||||
|
||||
public void setLedType(LedType ledType) {
|
||||
this.ledType = ledType;
|
||||
this.ledColors = null;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* The colours {@code count} LEDs are drawn lit with. These parts are drawn lit because unlit
|
||||
* they are a dark board with white specks on it, which reads as no particular component; running
|
||||
* the palette around the part also says at a glance which package it is fitted with, a WWA one
|
||||
* having no colour to show.
|
||||
*/
|
||||
protected Color[] getLedColors(int count) {
|
||||
if (ledColors == null || ledColors.length != count) {
|
||||
ledColors = buildLedGradient(count, getLedType().getGradient());
|
||||
}
|
||||
return ledColors;
|
||||
}
|
||||
}
|
||||
+45
-29
@@ -43,12 +43,7 @@ import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.LineStyle;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.common.VerticalAlignment;
|
||||
import org.diylc.components.AbstractLabeledComponent;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.Theme;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.*;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.core.annotations.PositiveNonZeroMeasureValidator;
|
||||
import org.diylc.core.measures.Size;
|
||||
@@ -63,7 +58,8 @@ import org.diylc.utils.Constants;
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponent<T> {
|
||||
public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponent<T> implements
|
||||
IHaveLength {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -169,10 +165,7 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
new Ellipse2D.Double((getFlipStanding() ? getPoints()[1] : getPoints()[0]).getX() - width / 2,
|
||||
(getFlipStanding() ? getPoints()[1] : getPoints()[0]).getY() - width / 2, width, width);
|
||||
shapeRect = body.getBounds();
|
||||
Composite oldComposite = g2d.getComposite();
|
||||
if (alpha < MAX_ALPHA) {
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
|
||||
}
|
||||
Composite oldComposite = applyAlpha(g2d, componentState);
|
||||
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getStandingBodyColor());
|
||||
g2d.fill(body);
|
||||
g2d.setComposite(oldComposite);
|
||||
@@ -259,19 +252,16 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
g2d.translate((getPoints()[0].getX() + getPoints()[1].getX() - length) / 2, (getPoints()[0].getY() + getPoints()[1].getY() - width) / 2);
|
||||
g2d.rotate(theta, length / 2, width / 2);
|
||||
// Draw body.
|
||||
Composite oldComposite = g2d.getComposite();
|
||||
if (alpha < MAX_ALPHA) {
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
|
||||
}
|
||||
if (bodyColor != null) {
|
||||
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
|
||||
Composite oldComposite = applyAlpha(g2d, componentState);
|
||||
if (getBodyColor() != null) {
|
||||
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getBodyColor());
|
||||
|
||||
drawingObserver.startTracking();
|
||||
|
||||
if (!outlineMode && ConfigurationManager.getInstance().readBoolean(IPlugInPort.HI_QUALITY_RENDER_KEY, false)) {
|
||||
Point2D p1 = new Point2D.Double(length / 2, 0);
|
||||
Point2D p2 = new Point2D.Double(length / 2, width);
|
||||
ShadedPaint paint = theta > 0 && theta < Math.PI ? new ShadedPaint(p2, p1, bodyColor) : new ShadedPaint(p1, p2, bodyColor);
|
||||
ShadedPaint paint = theta > 0 && theta < Math.PI ? new ShadedPaint(p2, p1, getBodyColor()) : new ShadedPaint(p1, p2, getBodyColor());
|
||||
Paint oldPaint = g2d.getPaint();
|
||||
g2d.setPaint(paint);
|
||||
g2d.fill(shape);
|
||||
@@ -303,17 +293,17 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
} else {
|
||||
finalBorderColor =
|
||||
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
|
||||
: borderColor;
|
||||
: getBorderColor();
|
||||
}
|
||||
g2d.setColor(finalBorderColor);
|
||||
|
||||
// if we are not filling the shape with color we need to ensure that we track the outline (e.g. with schematic symbols)
|
||||
if (bodyColor == null)
|
||||
if (getBodyColor() == null)
|
||||
drawingObserver.startTracking();
|
||||
|
||||
g2d.draw(shape);
|
||||
|
||||
if (bodyColor == null)
|
||||
if (getBodyColor() == null)
|
||||
drawingObserver.stopTracking();
|
||||
|
||||
if (decorateAboveBorder()) {
|
||||
@@ -367,8 +357,8 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
offset = -offset;
|
||||
}
|
||||
|
||||
drawingObserver.startTracking();
|
||||
if (getMoveLabel()) {
|
||||
drawingObserver.startTracking();
|
||||
g2d.setTransform(oldTransform);
|
||||
g2d.translate(getPoints()[2].getX(), getPoints()[2].getY());
|
||||
if (getLabelOriantation() != LabelOriantation.Horizontal) {
|
||||
@@ -378,19 +368,18 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
g2d.setTransform(oldTransform);
|
||||
} else {
|
||||
if (isStanding() || getLabelOriantation() == LabelOriantation.Horizontal) {
|
||||
drawingObserver.startTracking();
|
||||
g2d.setTransform(oldTransform);
|
||||
double x = (getPoints()[0].getX() + getPoints()[1].getX() - length) / 2.0;
|
||||
double y = (getPoints()[0].getY() + getPoints()[1].getY() - width) / 2.0;
|
||||
g2d.drawString(label, (int) (x + (length - textRect.getWidth()) / 2 + offset),
|
||||
(int) (y + calculateLabelYOffset(shapeRect, textRect, fontMetrics)));
|
||||
} else {
|
||||
drawingObserver.stopTracking();
|
||||
g2d.drawString(label, (int) (length - textRect.getWidth()) / 2 + offset,
|
||||
calculateLabelYOffset(shapeRect, textRect, fontMetrics));
|
||||
g2d.setTransform(oldTransform);
|
||||
}
|
||||
}
|
||||
drawingObserver.stopTracking();
|
||||
}
|
||||
// if (getLabelOriantation() == LabelOriantation.Horizontal) {
|
||||
//
|
||||
@@ -434,7 +423,7 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
Stroke stroke = null;
|
||||
switch (getStyle()) {
|
||||
case SOLID:
|
||||
stroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
|
||||
stroke = ObjectCache.getInstance().fetchStroke(thickness, getEndingStyle() == LineEndingStyle.Round ? BasicStroke.CAP_ROUND : BasicStroke.CAP_BUTT);
|
||||
break;
|
||||
case DASHED:
|
||||
stroke =
|
||||
@@ -445,10 +434,7 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
break;
|
||||
}
|
||||
|
||||
Composite oldComposite = g2d.getComposite();
|
||||
if (getAlpha() < MAX_ALPHA) {
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * getAlpha() / MAX_ALPHA));
|
||||
}
|
||||
Composite oldComposite = applyAlpha(g2d, componentState);
|
||||
if (shouldShadeLeads()) {
|
||||
Shape lineShape = stroke.createStrokedShape(line);
|
||||
|
||||
@@ -641,6 +627,32 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Size calculateLength() {
|
||||
|
||||
try {
|
||||
Point2D[] points = getPoints();
|
||||
if (points.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate distance between ending points
|
||||
double distanceInPixels = points[0].distance(points[1]);
|
||||
|
||||
// Convert to Size with appropriate unit (mm or in based on configuration)
|
||||
SizeUnit unit;
|
||||
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.METRIC_KEY, true)) {
|
||||
unit = SizeUnit.mm;
|
||||
} else {
|
||||
unit = SizeUnit.in;
|
||||
}
|
||||
|
||||
return Size.fromPixels(distanceInPixels, unit);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Width", defaultable = true, validatorClass = PositiveNonZeroMeasureValidator.class, sortOrder = 102)
|
||||
public Size getWidth() {
|
||||
return width;
|
||||
@@ -747,6 +759,10 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
|
||||
hideShortLeads = false;
|
||||
return hideShortLeads;
|
||||
}
|
||||
|
||||
protected LineEndingStyle getEndingStyle() {
|
||||
return LineEndingStyle.Round;
|
||||
}
|
||||
|
||||
public void setHideShortLeads(Boolean hideShortLeads) {
|
||||
this.hideShortLeads = hideShortLeads;
|
||||
|
||||
@@ -0,0 +1,938 @@
|
||||
/*
|
||||
*
|
||||
* DIY Layout Creator (DIYLC).
|
||||
* Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
*
|
||||
* This file is part of DIYLC.
|
||||
*
|
||||
* DIYLC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* DIYLC is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.diylc.components;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.awt.StringUtils;
|
||||
import org.diylc.common.HorizontalAlignment;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.common.Orientation;
|
||||
import org.diylc.common.VerticalAlignment;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.Theme;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.netlist.Node;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
/**
|
||||
* Base abstract class for Maker-focused boards, microcontrollers, sensors, displays, and breakout modules.
|
||||
*/
|
||||
public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Void> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final Color ARDUINO_TEAL = Color.decode("#00878F");
|
||||
public static final Color ARDUINO_BLUE = Color.decode("#015687");
|
||||
public static final Color RPI_GREEN = Color.decode("#1B5E20");
|
||||
public static final Color TEENSY_GREEN = Color.decode("#1B5E20");
|
||||
public static final Color ESP_BLACK = Color.decode("#3E3E3E");
|
||||
public static final Color WEMOS_BLUE = Color.decode("#006699");
|
||||
|
||||
public static Color PIN_COLOR = Color.decode("#00B2EE");
|
||||
public static Color PIN_BORDER_COLOR = PIN_COLOR.darker();
|
||||
public static Color HEADER_BODY_COLOR = Color.decode("#222222");
|
||||
public static Color HEADER_BORDER_COLOR = Color.decode("#111111");
|
||||
public static Color MOUNTING_HOLE_COLOR = Color.decode("#B8860B");
|
||||
public static Color MOUNTING_HOLE_BORDER = Color.decode("#8B6508");
|
||||
public static Color IC_BODY_COLOR = Color.decode("#1A1A1A");
|
||||
public static Color IC_TEXT_COLOR = Color.decode("#CCCCCC");
|
||||
public static Color SCREW_TERMINAL_COLOR = Color.decode("#90AB66");
|
||||
public static Color SCREW_TERMINAL_BORDER = Color.decode("#90AB66").darker();
|
||||
public static Color SCREW_CIRCLE_COLOR = LIGHT_METAL_COLOR;
|
||||
public static Color METAL_SHIELD_COLOR = Color.decode("#C0C0C0");
|
||||
public static Color METAL_SHIELD_BORDER = Color.decode("#909090");
|
||||
public static Color USB_METAL_COLOR = Color.decode("#D3D3D3");
|
||||
public static Color IC_BORDER_COLOR = Color.decode("#333333");
|
||||
public static Color PIN_MARKER_COLOR = Color.decode("#555555");
|
||||
public static Color METAL_LABEL_COLOR = Color.decode("#555555");
|
||||
public static Color ANTENNA_COLOR = Color.decode("#DAA520");
|
||||
public static Color ANTENNA_BG_COLOR = Color.decode("#1E1E1E");
|
||||
public static Color PAD_COLOR = GOLD_COLOR;
|
||||
public static Color CONNECTOR_PLASTIC_COLOR = Color.decode("#F5F5DC");
|
||||
public static Color CONNECTOR_PLASTIC_BORDER = Color.decode("#333333");
|
||||
public static Color BUTTON_BODY_COLOR = Color.decode("#383838");
|
||||
public static Color BUTTON_BORDER_COLOR = Color.decode("#666666");
|
||||
public static Color BUTTON_ACTUATOR_COLOR = Color.decode("#A0A0A0");
|
||||
public static Color DARK_PLASTIC_COLOR = Color.decode("#181818");
|
||||
public static Color DARK_PLASTIC_BORDER = Color.decode("#333333");
|
||||
public static Color RGB_LED_BODY_COLOR = Color.decode("#FDFEFE");
|
||||
public static Color RGB_LED_BODY_BORDER = Color.decode("#D0D3D4");
|
||||
public static Color RGB_LED_LENS_COLOR = Color.decode("#EAECEE");
|
||||
public static Color RGB_LED_LENS_BORDER = Color.decode("#BDC3C7");
|
||||
public static Color RGB_LED_CHIP_COLOR = Color.decode("#333333");
|
||||
// The colour wheel addressable RGB parts are drawn lit with. Treated as a loop, so a gradient
|
||||
// spread over any LED count closes without a seam.
|
||||
public static Color[] RGB_LED_GRADIENT = new Color[] {
|
||||
Color.decode("#FFEE00"), Color.decode("#FF8800"), Color.decode("#FF1122"),
|
||||
Color.decode("#AA22EE"), Color.decode("#2255FF"), Color.decode("#22DD44")};
|
||||
// A WWA package carries no colour die, so a part fitted with one cannot show the wheel above.
|
||||
// Its three anchors are the three dies themselves: warm white, white and amber.
|
||||
public static Color[] WWA_LED_GRADIENT = new Color[] {
|
||||
Color.decode("#FFE0BD"), Color.decode("#FFFFFF"), Color.decode("#FFAA00")};
|
||||
public static Color SILK_COLOR = Color.WHITE;
|
||||
|
||||
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
|
||||
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
|
||||
public static Size PAD_SIZE = new Size(0.065d, SizeUnit.in);
|
||||
public static Size HOLE_SIZE = new Size(0.7d, SizeUnit.mm);
|
||||
// the 5050 package shared by every addressable RGB part on a maker board
|
||||
public static Size RGB_LED_SIZE = new Size(5.0d, SizeUnit.mm);
|
||||
public static Size NOTCH_SIZE = new Size(0.9d, SizeUnit.mm);
|
||||
public static Size ANTENNA_WIDTH = new Size(15.0d, SizeUnit.mm);
|
||||
public static Size ANTENNA_LENGTH = new Size(7.0d, SizeUnit.mm);
|
||||
public static Size BUTTON_WIDTH = new Size(3.5d, SizeUnit.mm);
|
||||
public static Size BUTTON_LENGTH = new Size(3.0d, SizeUnit.mm);
|
||||
|
||||
// Standard USB Port Dimensions
|
||||
public static Size USB_MICRO_WIDTH = new Size(7.5d, SizeUnit.mm);
|
||||
public static Size USB_MICRO_LENGTH = new Size(5.6d, SizeUnit.mm);
|
||||
public static Size USB_MICRO_OVERHANG = new Size(0.5d, SizeUnit.mm);
|
||||
|
||||
public static Size USB_C_WIDTH = new Size(8.94d, SizeUnit.mm);
|
||||
public static Size USB_C_LENGTH = new Size(7.5d, SizeUnit.mm);
|
||||
public static Size USB_C_OVERHANG = new Size(0.5d, SizeUnit.mm);
|
||||
|
||||
public static Size USB_A_WIDTH = new Size(14.5d, SizeUnit.mm);
|
||||
public static Size USB_A_LENGTH = new Size(14.0d, SizeUnit.mm);
|
||||
public static Size USB_A_DUAL_LENGTH = new Size(17.5d, SizeUnit.mm);
|
||||
|
||||
public static Size USB_B_WIDTH = new Size(0.45d, SizeUnit.in);
|
||||
public static Size USB_B_LENGTH = new Size(0.51d, SizeUnit.in);
|
||||
public static Size USB_B_OVERHANG = new Size(0.14d, SizeUnit.in);
|
||||
|
||||
public static Size USB_MINI_WIDTH = new Size(0.30d, SizeUnit.in);
|
||||
public static Size USB_MINI_LENGTH = new Size(0.36d, SizeUnit.in);
|
||||
public static Size USB_MINI_OVERHANG = new Size(0.05d, SizeUnit.in);
|
||||
|
||||
public enum UsbPortType {
|
||||
MICRO,
|
||||
TYPE_C,
|
||||
TYPE_A,
|
||||
TYPE_B,
|
||||
MINI
|
||||
}
|
||||
|
||||
public static String SILK_FONT_FAMILY = "SansSerif";
|
||||
public static Font SILK_FONT_TINY = new Font(SILK_FONT_FAMILY, Font.BOLD, 9);
|
||||
public static Font SILK_FONT_SMALL = new Font(SILK_FONT_FAMILY, Font.PLAIN, 10);
|
||||
public static Font SILK_FONT = new Font(SILK_FONT_FAMILY, Font.BOLD, 11);
|
||||
public static Font SILK_FONT_LARGE = new Font(SILK_FONT_FAMILY, Font.BOLD, 13);
|
||||
public static Font SILK_FONT_TITLE = new Font(SILK_FONT_FAMILY, Font.BOLD, 20);
|
||||
// the toolbox icon is a fixed 32x32 box, so its lettering does not scale with the board
|
||||
public static Font ICON_FONT = new Font(SILK_FONT_FAMILY, Font.BOLD, 5);
|
||||
public static Font ICON_FONT_LARGE = new Font(SILK_FONT_FAMILY, Font.BOLD, 6);
|
||||
public static Size PIN_LABEL_OFFSET = new Size(1.6d, SizeUnit.mm);
|
||||
// diagonal labels are anchored by both coordinates at once, so they sit closer to the pad than
|
||||
// the straight ones without touching it
|
||||
public static Size PIN_LABEL_CORNER_OFFSET = new Size(1.0d, SizeUnit.mm);
|
||||
public static Size PIN_ROW_LABEL_OFFSET = new Size(2.0d, SizeUnit.mm);
|
||||
public static Font PIN_FONT = new Font(SILK_FONT_FAMILY, Font.PLAIN, 8);
|
||||
// labels along a row run across the board rather than down a column of pins, so they can afford
|
||||
// a size the tightly stacked column labels cannot
|
||||
public static Font PIN_ROW_FONT = new Font(SILK_FONT_FAMILY, Font.PLAIN, 9);
|
||||
// a label lying along its row has only the pin pitch to fit into, so it runs a size down from
|
||||
// the rotated ones and sits closer to the pin, which is all the room a shallow strip has
|
||||
public static Font PIN_ROW_FLAT_FONT = new Font(SILK_FONT_FAMILY, Font.PLAIN, 8);
|
||||
public static Size PIN_ROW_FLAT_LABEL_OFFSET = new Size(2.04d, SizeUnit.mm);
|
||||
// Clearance kept between text printed on a lit panel and the panel's edge. A readout is drawn
|
||||
// inside the glass rather than on board material, so unlike a silk label it has no neighbour to
|
||||
// collide with; the margin only stops the longest line from touching the frame.
|
||||
public static Size SCREEN_TEXT_MARGIN = new Size(0.6d, SizeUnit.mm);
|
||||
|
||||
protected Orientation orientation = Orientation.DEFAULT;
|
||||
protected Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
|
||||
protected Color bodyColor = Color.decode("#006699");
|
||||
|
||||
public AbstractMakerBoard() {
|
||||
super();
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Orientation")
|
||||
public Orientation getOrientation() {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
public void setOrientation(Orientation orientation) {
|
||||
this.orientation = orientation;
|
||||
updateControlPoints();
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Board Color")
|
||||
public Color getBodyColor() {
|
||||
return bodyColor;
|
||||
}
|
||||
|
||||
public void setBodyColor(Color bodyColor) {
|
||||
this.bodyColor = bodyColor;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return controlPoints[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
controlPoints[index].setLocation(point);
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return controlPoints.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.WHEN_SELECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canControlPointOverlap(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPointMoveFreely(int pointIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {}
|
||||
|
||||
/**
|
||||
* The variant this board is set to, or {@code null} for a board that is sold in only one form.
|
||||
* Boards name their variant property differently -- {@code Version}, {@code Size}, {@code Type} --
|
||||
* so each one that has one answers here for itself.
|
||||
*/
|
||||
protected String getVariantLabel() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* These boards have no value in the usual sense, so the BOM's value column carries the variant
|
||||
* instead. It is not cosmetic: {@code BomMaker} groups rows on type name plus value, so without
|
||||
* it every variant of a board collapses into a single row and a BOM cannot say which one to buy.
|
||||
*/
|
||||
@Override
|
||||
public String getValueForDisplay() {
|
||||
String variant = getVariantLabel();
|
||||
return variant == null ? "" : variant;
|
||||
}
|
||||
|
||||
protected abstract void updateControlPoints();
|
||||
|
||||
protected void invalidateCache() {}
|
||||
|
||||
/** Rotates relative point offsets around the first point by the board's orientation. */
|
||||
protected void rotatePoints(Point2D firstPoint, double[][] relativeOffsets) {
|
||||
controlPoints = new Point2D[relativeOffsets.length];
|
||||
controlPoints[0] = firstPoint;
|
||||
double theta = orientation.toRadians();
|
||||
AffineTransform tx = AffineTransform.getRotateInstance(theta, firstPoint.getX(), firstPoint.getY());
|
||||
for (int i = 1; i < relativeOffsets.length; i++) {
|
||||
Point2D p = new Point2D.Double(firstPoint.getX() + relativeOffsets[i][0], firstPoint.getY() + relativeOffsets[i][1]);
|
||||
tx.transform(p, p);
|
||||
controlPoints[i] = p;
|
||||
}
|
||||
}
|
||||
|
||||
/** Draws standard pin header pins at the given control point indices. */
|
||||
protected void drawPinHeader(Graphics2D g2d, int startIndex, int count, boolean outlineMode,
|
||||
IDrawingObserver drawingObserver) {
|
||||
if (outlineMode) return;
|
||||
int pinPx = (int) Math.round(PIN_SIZE.convertToPixels());
|
||||
drawingObserver.startTrackingContinuityArea(true);
|
||||
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
|
||||
Point2D p = controlPoints[i];
|
||||
g2d.setColor(HEADER_BODY_COLOR);
|
||||
g2d.fill(new Rectangle2D.Double(p.getX() - pinPx - 1, p.getY() - pinPx - 1, (pinPx + 1) * 2, (pinPx + 1) * 2));
|
||||
g2d.setColor(PIN_COLOR);
|
||||
g2d.fill(new Rectangle2D.Double(p.getX() - pinPx / 2.0, p.getY() - pinPx / 2.0, pinPx, pinPx));
|
||||
}
|
||||
drawingObserver.stopTrackingContinuityArea();
|
||||
|
||||
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
|
||||
Point2D p = controlPoints[i];
|
||||
g2d.setColor(PIN_BORDER_COLOR);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(new Rectangle2D.Double(p.getX() - pinPx / 2.0, p.getY() - pinPx / 2.0, pinPx, pinPx));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a pin name for silkscreen display, e.g. "3V3_1" to "3V3" and "A0 (ADC0)" to "A0".
|
||||
*
|
||||
* <p>Stripping the parenthesized annotation is the same operation the netlist performs, so it is
|
||||
* delegated to {@link Node#sanitizeNodeName(String)}. The silkscreen also drops the trailing
|
||||
* disambiguator, because a board prints "GND" on every ground pin even though the netlist has to
|
||||
* tell "GND_1" from "GND_2".
|
||||
*/
|
||||
public static String getDisplayPinLabel(String name) {
|
||||
String sanitized = Node.sanitizeNodeName(name);
|
||||
if (sanitized == null) {
|
||||
return "";
|
||||
}
|
||||
int underscoreIdx = sanitized.indexOf('_');
|
||||
return (underscoreIdx == -1 ? sanitized : sanitized.substring(0, underscoreIdx)).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Silkscreen text printed next to a pin. Defaults to the node name with its annotation and
|
||||
* disambiguator stripped. A board whose silkscreen genuinely differs -- the ESP DevKits print
|
||||
* bare GPIO numbers while their nodes carry the full function list -- overrides this.
|
||||
*/
|
||||
protected String getSilkPinLabel(int index) {
|
||||
return getDisplayPinLabel(getControlPointNodeName(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws rotated pin names beside the pins of a dual-row DIP or header board.
|
||||
*
|
||||
* @param g2d Graphics2D context (already transformed for board orientation)
|
||||
* @param x Unrotated top-left pin X coordinate (P0.getX())
|
||||
* @param y Unrotated top-left pin Y coordinate (P0.getY())
|
||||
* @param offsets Array of [x, y] relative offsets for all control points
|
||||
* @param silkColor Silkscreen text color
|
||||
*/
|
||||
protected void drawPinLabels(Graphics2D g2d, double x, double y, double[][] offsets, Color silkColor) {
|
||||
if (offsets == null || offsets.length == 0) return;
|
||||
drawPinLabels(g2d, x, y, offsets, offsets.length / 2, silkColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant for boards whose two pin rows do not take up the whole control point array, such as a
|
||||
* board that carries an ICSP block or a connector after them. Control points past the two rows
|
||||
* are left unlabelled.
|
||||
*
|
||||
* @param g2d Graphics2D context (already transformed for board orientation)
|
||||
* @param x Unrotated top-left pin X coordinate (P0.getX())
|
||||
* @param y Unrotated top-left pin Y coordinate (P0.getY())
|
||||
* @param offsets Array of [x, y] relative offsets for all control points
|
||||
* @param pinsPerRow Number of pins in each of the two rows
|
||||
* @param silkColor Silkscreen text color
|
||||
*/
|
||||
protected void drawPinLabels(Graphics2D g2d, double x, double y, double[][] offsets,
|
||||
int pinsPerRow, Color silkColor) {
|
||||
if (offsets == null || offsets.length == 0) return;
|
||||
double labelOffset = PIN_LABEL_OFFSET.convertToPixels();
|
||||
|
||||
g2d.setColor(silkColor);
|
||||
g2d.setFont(PIN_FONT);
|
||||
|
||||
for (int i = 0; i < 2 * pinsPerRow && i < offsets.length; i++) {
|
||||
String label = getSilkPinLabel(i);
|
||||
if (label == null || label.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
boolean leftRow = i < pinsPerRow;
|
||||
double pinX = x + offsets[i][0];
|
||||
double pinY = y + offsets[i][1];
|
||||
double textX = leftRow ? (pinX + labelOffset) : (pinX - labelOffset);
|
||||
double textY = pinY;
|
||||
|
||||
// the two rows are mirrored so that each label reads away from the pin it belongs to, the
|
||||
// way a board prints them
|
||||
AffineTransform oldLabelTx = g2d.getTransform();
|
||||
g2d.rotate(leftRow ? Math.PI / 2 : -Math.PI / 2, textX, textY);
|
||||
StringUtils.drawCenteredText(g2d, label, textX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
|
||||
g2d.setTransform(oldLabelTx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant for boards too crowded for labels that stand square to their row. Each label is turned
|
||||
* 45 degrees and tucked into the corner of its pad on the side the row reads towards, which is
|
||||
* how the Teensy 4.1 fits its pad names between two rows a tenth of an inch apart.
|
||||
*
|
||||
* @param g2d Graphics2D context (already transformed for board orientation)
|
||||
* @param x Unrotated top-left pin X coordinate (P0.getX())
|
||||
* @param y Unrotated top-left pin Y coordinate (P0.getY())
|
||||
* @param offsets Array of [x, y] relative offsets for all control points
|
||||
* @param pinsPerRow Number of pins in each of the two rows
|
||||
* @param silkColor Silkscreen text color
|
||||
*/
|
||||
protected void drawDiagonalPinLabels(Graphics2D g2d, double x, double y, double[][] offsets,
|
||||
int pinsPerRow, Color silkColor) {
|
||||
if (offsets == null || offsets.length == 0) return;
|
||||
double cornerOffset = PIN_LABEL_CORNER_OFFSET.convertToPixels();
|
||||
double straightOffset = PIN_LABEL_OFFSET.convertToPixels();
|
||||
|
||||
g2d.setColor(silkColor);
|
||||
g2d.setFont(PIN_FONT);
|
||||
|
||||
for (int i = 0; i < 2 * pinsPerRow && i < offsets.length; i++) {
|
||||
String label = getSilkPinLabel(i);
|
||||
if (label == null || label.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
boolean leftRow = i < pinsPerRow;
|
||||
// the pad that starts each row has the board edge above it rather than another pad, so it
|
||||
// keeps the square on label the rest of the row has no room for
|
||||
boolean square = i == 0 || i == pinsPerRow;
|
||||
double offset = square ? straightOffset : cornerOffset;
|
||||
double textX = x + offsets[i][0] + (leftRow ? offset : -offset);
|
||||
double textY = y + offsets[i][1] - (square ? 0 : cornerOffset);
|
||||
|
||||
// the two rows slant away from each other so that each label runs along the diagonal gap
|
||||
// between its own pad and the one above it
|
||||
AffineTransform oldLabelTx = g2d.getTransform();
|
||||
g2d.rotate((square ? Math.PI / 2 : Math.PI / 4) * (leftRow ? 1 : -1), textX, textY);
|
||||
StringUtils.drawCenteredText(g2d, label, textX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
|
||||
g2d.setTransform(oldLabelTx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws rotated pin names beside a horizontal row, the way Arduino-style boards print theirs.
|
||||
* Labels read top to bottom, centred on their pin and starting {@link #PIN_ROW_LABEL_OFFSET}
|
||||
* away from it.
|
||||
*
|
||||
* @param g2d Graphics2D context (already transformed for board orientation)
|
||||
* @param x Unrotated first control point X coordinate (P0.getX())
|
||||
* @param y Unrotated first control point Y coordinate (P0.getY())
|
||||
* @param offsets Array of [x, y] relative offsets for all control points
|
||||
* @param startIndex Index of the first pin of the row
|
||||
* @param count Number of pins in the row
|
||||
* @param below True to print the labels below the row, false to print them above it
|
||||
* @param silkColor Silkscreen text color
|
||||
*/
|
||||
protected void drawRowPinLabels(Graphics2D g2d, double x, double y, double[][] offsets,
|
||||
int startIndex, int count, boolean below, Color silkColor) {
|
||||
if (offsets == null || offsets.length == 0) return;
|
||||
double labelOffset = PIN_ROW_LABEL_OFFSET.convertToPixels();
|
||||
|
||||
g2d.setColor(silkColor);
|
||||
g2d.setFont(PIN_ROW_FONT);
|
||||
|
||||
for (int i = startIndex; i < startIndex + count && i < offsets.length; i++) {
|
||||
String label = getSilkPinLabel(i);
|
||||
if (label == null || label.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
double pinX = x + offsets[i][0];
|
||||
double pinY = y + offsets[i][1];
|
||||
|
||||
// the quarter turn maps the text advance direction onto -y, so a label above the row is
|
||||
// anchored by its first character and one below the row by its last, which keeps both of
|
||||
// them clear of the pin by the same gap
|
||||
AffineTransform oldLabelTx = g2d.getTransform();
|
||||
g2d.rotate(-Math.PI / 2, pinX, pinY);
|
||||
StringUtils.drawCenteredText(g2d, label, pinX + (below ? -labelOffset : labelOffset), pinY,
|
||||
below ? HorizontalAlignment.RIGHT : HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
|
||||
g2d.setTransform(oldLabelTx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Baseline for labels that have to sit on one line. {@link StringUtils}'s CENTER alignment
|
||||
* centres each string on its own glyph box, so a name with a descender rides higher than the
|
||||
* names beside it. Measuring one reference glyph gives every label the same baseline.
|
||||
*/
|
||||
private static double getLabelBaseline(Graphics2D g2d, double centreY) {
|
||||
Rectangle bounds = g2d.getFont()
|
||||
.createGlyphVector(g2d.getFontRenderContext(), "X").getVisualBounds().getBounds();
|
||||
return centreY - bounds.height / 2.0 - bounds.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws pin names lying flat along a horizontal row, for a board whose strip beside the header is
|
||||
* shallower than the longest name is long. {@link #drawRowPinLabels} stands its labels on end and
|
||||
* has the whole depth of the board to grow into; these have only the pin pitch, so they run in a
|
||||
* smaller font.
|
||||
*
|
||||
* @param g2d Graphics2D context (already transformed for board orientation)
|
||||
* @param x Unrotated first control point X coordinate (P0.getX())
|
||||
* @param y Unrotated first control point Y coordinate (P0.getY())
|
||||
* @param offsets Array of [x, y] relative offsets for all control points
|
||||
* @param startIndex Index of the first pin of the row
|
||||
* @param count Number of pins in the row
|
||||
* @param below True to print the labels below the row, false to print them above it
|
||||
* @param silkColor Silkscreen text color
|
||||
*/
|
||||
protected void drawFlatRowPinLabels(Graphics2D g2d, double x, double y, double[][] offsets,
|
||||
int startIndex, int count, boolean below, Color silkColor) {
|
||||
if (offsets == null || offsets.length == 0) {
|
||||
return;
|
||||
}
|
||||
double labelOffset = PIN_ROW_FLAT_LABEL_OFFSET.convertToPixels();
|
||||
|
||||
g2d.setColor(silkColor);
|
||||
g2d.setFont(PIN_ROW_FLAT_FONT);
|
||||
|
||||
for (int i = startIndex; i < startIndex + count && i < offsets.length; i++) {
|
||||
String label = getSilkPinLabel(i);
|
||||
if (label == null || label.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
double pinX = x + offsets[i][0];
|
||||
double pinY = y + offsets[i][1] + (below ? labelOffset : -labelOffset);
|
||||
// TOP passes the y straight through as the baseline, which is what keeps the row level
|
||||
StringUtils.drawCenteredText(g2d, label, pinX, getLabelBaseline(g2d, pinY),
|
||||
HorizontalAlignment.CENTER, VerticalAlignment.TOP);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a PCB terminal block matching the visuals of the PCBTerminalBlock component.
|
||||
*
|
||||
* @param g2d Graphics2D context
|
||||
* @param startIndex First control point index
|
||||
* @param count Number of positions in this terminal block
|
||||
* @param isHorizontal true if the block runs horizontally (along X), false if vertically
|
||||
* @param wireEntryOffset Direction/offset from pins to the wire entry edge
|
||||
* @param blockDepth Total depth of the green body in pixels
|
||||
* @param outlineMode Outline mode flag
|
||||
* @param drawingObserver Observer
|
||||
*/
|
||||
protected void drawTerminalBlock(Graphics2D g2d, int startIndex, int count, boolean isHorizontal,
|
||||
double wireEntryOffset, double blockDepth, boolean outlineMode, IDrawingObserver drawingObserver) {
|
||||
if (outlineMode) return;
|
||||
if (count <= 0 || startIndex < 0 || startIndex >= controlPoints.length) return;
|
||||
|
||||
Point2D p0 = controlPoints[startIndex];
|
||||
Point2D pLast = controlPoints[Math.min(startIndex + count - 1, controlPoints.length - 1)];
|
||||
|
||||
double pitchPx = count > 1 ?
|
||||
(isHorizontal ? Math.abs(pLast.getX() - p0.getX()) / (count - 1) : Math.abs(pLast.getY() - p0.getY()) / (count - 1))
|
||||
: new Size(5.08d, SizeUnit.mm).convertToPixels();
|
||||
if (pitchPx <= 0) {
|
||||
pitchPx = new Size(5.08d, SizeUnit.mm).convertToPixels();
|
||||
}
|
||||
|
||||
double minX = Math.min(p0.getX(), pLast.getX());
|
||||
double minY = Math.min(p0.getY(), pLast.getY());
|
||||
double maxX = Math.max(p0.getX(), pLast.getX());
|
||||
double maxY = Math.max(p0.getY(), pLast.getY());
|
||||
|
||||
Rectangle2D outerBody;
|
||||
Rectangle2D innerCompartment;
|
||||
|
||||
if (isHorizontal) {
|
||||
double bx = minX - pitchPx / 2.0;
|
||||
double bw = (maxX - minX) + pitchPx;
|
||||
double by = wireEntryOffset >= 0 ? minY - (blockDepth - pitchPx) / 2.0 : minY - (blockDepth - pitchPx / 2.0);
|
||||
double bh = blockDepth;
|
||||
outerBody = new Rectangle2D.Double(bx, by, bw, bh);
|
||||
innerCompartment = new Rectangle2D.Double(bx, minY - pitchPx / 2.0, bw, pitchPx);
|
||||
} else {
|
||||
double by = minY - pitchPx / 2.0;
|
||||
double bh = (maxY - minY) + pitchPx;
|
||||
double bx = wireEntryOffset >= 0 ? minX - (blockDepth - pitchPx) / 2.0 : minX - (blockDepth - pitchPx / 2.0);
|
||||
double bw = blockDepth;
|
||||
outerBody = new Rectangle2D.Double(bx, by, bw, bh);
|
||||
innerCompartment = new Rectangle2D.Double(minX - pitchPx / 2.0, by, pitchPx, bh);
|
||||
}
|
||||
|
||||
g2d.setColor(SCREW_TERMINAL_COLOR);
|
||||
g2d.fill(outerBody);
|
||||
g2d.setColor(SCREW_TERMINAL_BORDER);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(innerCompartment);
|
||||
g2d.draw(outerBody);
|
||||
|
||||
drawScrewTerminals(g2d, startIndex, count, pitchPx, outlineMode, drawingObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws screw terminal contacts matching the PCBTerminalBlock component.
|
||||
*/
|
||||
protected void drawScrewTerminals(Graphics2D g2d, int startIndex, int count, double pitchPx, boolean outlineMode, IDrawingObserver drawingObserver) {
|
||||
if (outlineMode) return;
|
||||
double effectivePitch = pitchPx > 0 ? pitchPx : PIN_SPACING.convertToPixels();
|
||||
int circleDiameter = getClosestOdd((int) (effectivePitch * 3d / 5));
|
||||
|
||||
drawingObserver.startTrackingContinuityArea(true);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f));
|
||||
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
|
||||
Point2D p = controlPoints[i];
|
||||
g2d.setColor(SCREW_CIRCLE_COLOR);
|
||||
g2d.fillOval((int) Math.round(p.getX() - circleDiameter / 2.0), (int) Math.round(p.getY() - circleDiameter / 2.0),
|
||||
circleDiameter, circleDiameter);
|
||||
g2d.setColor(SCREW_CIRCLE_COLOR.darker());
|
||||
g2d.drawLine((int) (p.getX() + Math.cos(Math.PI / 4) * circleDiameter / 2.0),
|
||||
(int) (p.getY() + Math.sin(Math.PI / 4) * circleDiameter / 2.0),
|
||||
(int) (p.getX() + Math.cos(5 * Math.PI / 4) * circleDiameter / 2.0),
|
||||
(int) (p.getY() + Math.sin(5 * Math.PI / 4) * circleDiameter / 2.0));
|
||||
}
|
||||
drawingObserver.stopTrackingContinuityArea();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spreads {@link #RGB_LED_GRADIENT} evenly over {@code count} LEDs, treating the anchors as a
|
||||
* loop so the last LED runs back into the first. Called once per palette at class load.
|
||||
*/
|
||||
protected static Color[] buildLedGradient(int count) {
|
||||
return buildLedGradient(count, RGB_LED_GRADIENT);
|
||||
}
|
||||
|
||||
/** As above, over a palette of the caller's choosing. */
|
||||
protected static Color[] buildLedGradient(int count, Color[] anchors) {
|
||||
Color[] colors = new Color[count];
|
||||
int anchorCount = anchors.length;
|
||||
for (int i = 0; i < count; i++) {
|
||||
double position = (double) i * anchorCount / count;
|
||||
int anchor = (int) position;
|
||||
double blend = position - anchor;
|
||||
Color from = anchors[anchor];
|
||||
Color to = anchors[(anchor + 1) % anchorCount];
|
||||
colors[i] = new Color(
|
||||
(int) Math.round(from.getRed() + (to.getRed() - from.getRed()) * blend),
|
||||
(int) Math.round(from.getGreen() + (to.getGreen() - from.getGreen()) * blend),
|
||||
(int) Math.round(from.getBlue() + (to.getBlue() - from.getBlue()) * blend));
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws through-hole solder pads with drill holes.
|
||||
*
|
||||
* @param g2d Graphics2D context
|
||||
* @param startIndex First control point index
|
||||
* @param count Number of pads to draw
|
||||
* @param squarePin1 If true, the first pad is drawn square
|
||||
* @param outlineMode Outline mode flag
|
||||
* @param drawingObserver Observer
|
||||
*/
|
||||
protected void drawPcbSolderPads(Graphics2D g2d, int startIndex, int count, boolean squarePin1, boolean outlineMode, IDrawingObserver drawingObserver) {
|
||||
if (outlineMode) return;
|
||||
int diameter = getClosestOdd((int) Math.round(PAD_SIZE.convertToPixels()));
|
||||
int holeDiameter = getClosestOdd((int) Math.round(HOLE_SIZE.convertToPixels()));
|
||||
|
||||
drawingObserver.startTrackingContinuityArea(true);
|
||||
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
|
||||
Point2D p = controlPoints[i];
|
||||
if (squarePin1 && i == startIndex) {
|
||||
g2d.setColor(PAD_COLOR);
|
||||
g2d.fill(new Rectangle2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
|
||||
g2d.setColor(PAD_COLOR.darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(new Rectangle2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
|
||||
} else {
|
||||
g2d.setColor(PAD_COLOR);
|
||||
g2d.fill(new Ellipse2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
|
||||
g2d.setColor(PAD_COLOR.darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(new Ellipse2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
|
||||
}
|
||||
|
||||
g2d.setColor(Constants.CANVAS_COLOR);
|
||||
g2d.fill(new Ellipse2D.Double(p.getX() - holeDiameter / 2.0, p.getY() - holeDiameter / 2.0, holeDiameter, holeDiameter));
|
||||
g2d.setColor(PAD_COLOR.darker());
|
||||
g2d.draw(new Ellipse2D.Double(p.getX() - holeDiameter / 2.0, p.getY() - holeDiameter / 2.0, holeDiameter, holeDiameter));
|
||||
}
|
||||
drawingObserver.stopTrackingContinuityArea();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws rectangular surface pads: bare copper with no drill through it, which is what a part
|
||||
* meant to be soldered to directly carries rather than the plated holes
|
||||
* {@link #drawPcbSolderPads} draws. Unlike a round pad a rectangular one has an orientation, so
|
||||
* each is turned with the board.
|
||||
*
|
||||
* @param padWidth Pad size across the board, which for an edge pad is its reach inward
|
||||
* @param padLength Pad size along the row of pads
|
||||
*/
|
||||
protected void drawSurfacePads(Graphics2D g2d, int startIndex, int count, Size padWidth,
|
||||
Size padLength, boolean outlineMode, IDrawingObserver drawingObserver) {
|
||||
if (outlineMode) {
|
||||
return;
|
||||
}
|
||||
double w = padWidth.convertToPixels();
|
||||
double h = padLength.convertToPixels();
|
||||
double theta = orientation.toRadians();
|
||||
|
||||
drawingObserver.startTrackingContinuityArea(true);
|
||||
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
|
||||
Point2D p = controlPoints[i];
|
||||
if (theta != 0) {
|
||||
g2d.rotate(theta, p.getX(), p.getY());
|
||||
}
|
||||
|
||||
Rectangle2D pad = new Rectangle2D.Double(p.getX() - w / 2.0, p.getY() - h / 2.0, w, h);
|
||||
g2d.setColor(PAD_COLOR);
|
||||
g2d.fill(pad);
|
||||
g2d.setColor(PAD_COLOR.darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(pad);
|
||||
|
||||
// turned back rather than saving the transform, as the LED painter does
|
||||
if (theta != 0) {
|
||||
g2d.rotate(-theta, p.getX(), p.getY());
|
||||
}
|
||||
}
|
||||
drawingObserver.stopTrackingContinuityArea();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the pads a cut leaves on a part sold by the length. The uncut pad is a pill lying along
|
||||
* the part with the cut through its middle, so each end keeps half of one: flat against the
|
||||
* edge, rounded inward. Built as the pill and then cut rather than drawn as a half, so that it
|
||||
* stays right at both extremes -- half of a pill no longer than it is wide is a semicircle, with
|
||||
* no special case.
|
||||
*
|
||||
* <p>Both sizes are in pixels rather than {@link Size}, a part that caps its pad against the room
|
||||
* a variant leaves having already done the arithmetic.
|
||||
*
|
||||
* @param padWidth Pad size across the part
|
||||
* @param padLength Pad size along the part, before the cut -- half of this is drawn
|
||||
* @param reachesRight Whether the surviving half reaches to the right of the cut
|
||||
*/
|
||||
protected void drawBisectedPads(Graphics2D g2d, int startIndex, int count, double padWidth,
|
||||
double padLength, boolean reachesRight, boolean outlineMode,
|
||||
IDrawingObserver drawingObserver) {
|
||||
if (outlineMode) {
|
||||
return;
|
||||
}
|
||||
double w = padWidth;
|
||||
double depth = padLength / 2.0;
|
||||
double theta = orientation.toRadians();
|
||||
|
||||
drawingObserver.startTrackingContinuityArea(true);
|
||||
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
|
||||
Point2D p = controlPoints[i];
|
||||
if (theta != 0) {
|
||||
g2d.rotate(theta, p.getX(), p.getY());
|
||||
}
|
||||
|
||||
Area pad = new Area(new RoundRectangle2D.Double(p.getX() - depth, p.getY() - w / 2.0,
|
||||
depth * 2, w, w, w));
|
||||
pad.intersect(new Area(new Rectangle2D.Double(
|
||||
reachesRight ? p.getX() : p.getX() - depth, p.getY() - w / 2.0, depth, w)));
|
||||
|
||||
g2d.setColor(PAD_COLOR);
|
||||
g2d.fill(pad);
|
||||
g2d.setColor(PAD_COLOR.darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(pad);
|
||||
|
||||
if (theta != 0) {
|
||||
g2d.rotate(-theta, p.getX(), p.getY());
|
||||
}
|
||||
}
|
||||
drawingObserver.stopTrackingContinuityArea();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad, drill-hole and edge-notch diameters used by {@link #drawCastellatedPads} and
|
||||
* {@link #subtractCastellationNotches}. A board whose castellations differ from the package
|
||||
* defaults overrides these rather than reimplementing the pad geometry.
|
||||
*/
|
||||
protected Size getCastellatedPadSize() {
|
||||
return PAD_SIZE;
|
||||
}
|
||||
|
||||
protected Size getCastellatedHoleSize() {
|
||||
return HOLE_SIZE;
|
||||
}
|
||||
|
||||
protected Size getCastellatedNotchSize() {
|
||||
return NOTCH_SIZE;
|
||||
}
|
||||
|
||||
protected Color getCastellatedPadColor() {
|
||||
return PAD_COLOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws one column of castellated edge pads: a round through-hole pad joined to the board edge by
|
||||
* a stub, with a semicircular notch bitten out of the edge. Pair it with
|
||||
* {@link #subtractCastellationNotches} so the outline and the pads agree.
|
||||
*
|
||||
* <p>Unlike the control-point based pad helpers this works in unrotated board coordinates, so it
|
||||
* has to be called while the board rotation is still applied to {@code g2d}.
|
||||
*
|
||||
* @param g2d Graphics2D context, already transformed for board orientation
|
||||
* @param pinX Unrotated X coordinate of the pad column
|
||||
* @param pinY Unrotated Y coordinate of the first pad
|
||||
* @param count Number of pads in the column
|
||||
* @param spacing Pad pitch in pixels
|
||||
* @param edgeX Unrotated X coordinate of the board edge the pads reach; the stub is drawn toward
|
||||
* it, so it may lie on either side of {@code pinX}
|
||||
*/
|
||||
protected void drawCastellatedPads(Graphics2D g2d, double pinX, double pinY, int count, double spacing,
|
||||
double edgeX, boolean outlineMode, IDrawingObserver drawingObserver) {
|
||||
if (outlineMode) return;
|
||||
|
||||
int padD = getClosestOdd((int) Math.round(getCastellatedPadSize().convertToPixels()));
|
||||
int holeD = getClosestOdd((int) Math.round(getCastellatedHoleSize().convertToPixels()));
|
||||
int notchD = getClosestOdd((int) Math.round(getCastellatedNotchSize().convertToPixels()));
|
||||
double padR = padD / 2.0;
|
||||
double holeR = holeD / 2.0;
|
||||
double notchR = notchD / 2.0;
|
||||
double stubX = Math.min(pinX, edgeX);
|
||||
double stubW = Math.abs(edgeX - pinX);
|
||||
Color padColor = getCastellatedPadColor();
|
||||
|
||||
drawingObserver.startTrackingContinuityArea(true);
|
||||
for (int i = 0; i < count; i++) {
|
||||
double py = pinY + i * spacing;
|
||||
|
||||
Area padArea = new Area(new Rectangle2D.Double(stubX, py - padR, stubW, padD));
|
||||
padArea.add(new Area(new Ellipse2D.Double(pinX - padR, py - padR, padD, padD)));
|
||||
padArea.subtract(new Area(new Ellipse2D.Double(edgeX - notchR, py - notchR, notchD, notchD)));
|
||||
padArea.subtract(new Area(new Ellipse2D.Double(pinX - holeR, py - holeR, holeD, holeD)));
|
||||
|
||||
g2d.setColor(padColor);
|
||||
g2d.fill(padArea);
|
||||
g2d.setColor(padColor.darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(padArea);
|
||||
|
||||
// the drill hole
|
||||
g2d.setColor(Constants.CANVAS_COLOR);
|
||||
g2d.fill(new Ellipse2D.Double(pinX - holeR, py - holeR, holeD, holeD));
|
||||
g2d.setColor(padColor.darker());
|
||||
g2d.draw(new Ellipse2D.Double(pinX - holeR, py - holeR, holeD, holeD));
|
||||
}
|
||||
drawingObserver.stopTrackingContinuityArea();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bites the semicircular castellation notches for one column of pads out of a board outline,
|
||||
* matching what {@link #drawCastellatedPads} draws along the same edge.
|
||||
*/
|
||||
protected void subtractCastellationNotches(Area boardArea, double pinY, int count, double spacing,
|
||||
double edgeX) {
|
||||
int notchD = getClosestOdd((int) Math.round(getCastellatedNotchSize().convertToPixels()));
|
||||
double notchR = notchD / 2.0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
double py = pinY + i * spacing;
|
||||
boardArea.subtract(new Area(new Ellipse2D.Double(edgeX - notchR, py - notchR, notchD, notchD)));
|
||||
}
|
||||
}
|
||||
|
||||
protected Color getFinalBorderColor(ComponentState componentState, boolean outlineMode) {
|
||||
if (outlineMode) {
|
||||
Theme theme = Constants.DEFAULT_THEME;
|
||||
try {
|
||||
Theme t = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
|
||||
if (t != null) theme = t;
|
||||
} catch (Exception ignored) {}
|
||||
return (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) ? SELECTION_COLOR : theme.getOutlineColor();
|
||||
}
|
||||
return (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) ? SELECTION_COLOR : bodyColor.darker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unrotated body shape in local coordinate space (relative to unrotated p0).
|
||||
*/
|
||||
public abstract Shape getBodyShape();
|
||||
|
||||
@Override
|
||||
public Rectangle2D getCachingBounds() {
|
||||
double minX = Double.MAX_VALUE;
|
||||
double maxX = -Double.MAX_VALUE;
|
||||
double minY = Double.MAX_VALUE;
|
||||
double maxY = -Double.MAX_VALUE;
|
||||
|
||||
if (controlPoints != null) {
|
||||
for (Point2D p : controlPoints) {
|
||||
if (p != null) {
|
||||
if (p.getX() < minX) minX = p.getX();
|
||||
if (p.getX() > maxX) maxX = p.getX();
|
||||
if (p.getY() < minY) minY = p.getY();
|
||||
if (p.getY() > maxY) maxY = p.getY();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape bodyShape = getBodyShape();
|
||||
if (bodyShape != null && controlPoints != null && controlPoints.length > 0 && controlPoints[0] != null) {
|
||||
Point2D p0 = controlPoints[0];
|
||||
AffineTransform tx = AffineTransform.getRotateInstance(orientation.toRadians(), p0.getX(), p0.getY());
|
||||
Shape rotated = tx.createTransformedShape(bodyShape);
|
||||
Rectangle2D b = rotated.getBounds2D();
|
||||
if (b.getMinX() < minX) minX = b.getMinX();
|
||||
if (b.getMaxX() > maxX) maxX = b.getMaxX();
|
||||
if (b.getMinY() < minY) minY = b.getMinY();
|
||||
if (b.getMaxY() > maxY) maxY = b.getMaxY();
|
||||
}
|
||||
|
||||
if (minX == Double.MAX_VALUE) {
|
||||
Point2D p0 = (controlPoints != null && controlPoints.length > 0 && controlPoints[0] != null)
|
||||
? controlPoints[0] : new Point2D.Double(0, 0);
|
||||
return new Rectangle2D.Double(p0.getX() - 50, p0.getY() - 50, 100, 100);
|
||||
}
|
||||
|
||||
int margin = 50;
|
||||
return new Rectangle2D.Double(minX - margin, minY - margin, (maxX - minX) + 2 * margin, (maxY - minY) + 2 * margin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Which addressable package a part is fitted with. The three are pin-compatible and share the
|
||||
* 5050 footprint, so this changes only what the LEDs can emit and what the BOM asks for.
|
||||
*
|
||||
* <p>RGBW adds a fourth, white die to the RGB three and is sold by the tint of that die; WWA
|
||||
* replaces the colour dies altogether with warm white, white and amber.
|
||||
*/
|
||||
public enum LedType {
|
||||
RGB("RGB"),
|
||||
RGBW_WARM("RGBW Warm White"),
|
||||
RGBW_NATURAL("RGBW Natural White"),
|
||||
RGBW_COOL("RGBW Cool White"),
|
||||
WWA("WWA");
|
||||
|
||||
private final String label;
|
||||
|
||||
LedType(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@Override public String toString() { return label; }
|
||||
|
||||
/** True for the packages that carry a white die beside the colour ones. */
|
||||
public boolean hasWhiteDie() { return this != RGB && this != WWA; }
|
||||
|
||||
/** The wheel a part fitted with this package is drawn lit with. */
|
||||
public Color[] getGradient() { return this == WWA ? WWA_LED_GRADIENT : RGB_LED_GRADIENT; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.diylc.components;
|
||||
|
||||
public enum LineEndingStyle {
|
||||
Round, Flat
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
/*
|
||||
*
|
||||
* DIY Layout Creator (DIYLC).
|
||||
* Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
*
|
||||
* This file is part of DIYLC.
|
||||
*
|
||||
* DIYLC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* DIYLC is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.diylc.components;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.Path2D;
|
||||
|
||||
/**
|
||||
* Vector artwork for the manufacturer logos silkscreened onto maker boards. The paths are traced
|
||||
* from the official brand marks and are drawn in white, the way the boards themselves print them.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class MakerBoardLogos {
|
||||
|
||||
private static final Area RASPBERRY_PI_LOGO_AREA = createRaspberryPiLogoArea();
|
||||
|
||||
private MakerBoardLogos() {}
|
||||
|
||||
/**
|
||||
* Helper to draw the official Arduino infinity logo (white infinity symbol with '-' and '+').
|
||||
* Default bounding box size is 95.56 x 45.33 px.
|
||||
*
|
||||
* @param g2d Graphics2D context
|
||||
* @param x Top-left X coordinate of the logo bounding box
|
||||
* @param y Top-left Y coordinate of the logo bounding box
|
||||
*/
|
||||
public static void drawArduinoLogo(Graphics2D g2d, double x, double y) {
|
||||
drawArduinoLogo(g2d, x, y, 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to draw the official Arduino infinity logo with custom scaling.
|
||||
*
|
||||
* @param g2d Graphics2D context
|
||||
* @param x Top-left X coordinate of the logo bounding box
|
||||
* @param y Top-left Y coordinate of the logo bounding box
|
||||
* @param scale Scale factor (1.0 = 95.56 x 45.33 px)
|
||||
*/
|
||||
public static void drawArduinoLogo(Graphics2D g2d, double x, double y, double scale) {
|
||||
AffineTransform oldTx = g2d.getTransform();
|
||||
g2d.translate(x, y);
|
||||
if (scale != 1.0) {
|
||||
g2d.scale(scale, scale);
|
||||
}
|
||||
|
||||
Path2D.Double infinityBody = new Path2D.Double(Path2D.WIND_EVEN_ODD);
|
||||
// Outer loop
|
||||
infinityBody.moveTo(47.56, 14.67);
|
||||
infinityBody.curveTo(50.00, 12.00, 51.78, 9.78, 54.00, 7.78);
|
||||
infinityBody.curveTo(62.22, 0.44, 71.56, -2.44, 82.22, 2.22);
|
||||
infinityBody.curveTo(92.22, 6.44, 97.78, 16.89, 95.56, 27.56);
|
||||
infinityBody.curveTo(93.56, 37.33, 84.00, 44.89, 73.78, 45.33);
|
||||
infinityBody.curveTo(64.22, 45.56, 56.89, 41.33, 50.89, 34.44);
|
||||
infinityBody.curveTo(50.00, 33.33, 49.11, 32.22, 48.00, 30.89);
|
||||
infinityBody.curveTo(47.11, 31.78, 46.44, 32.67, 46.00, 33.33);
|
||||
infinityBody.curveTo(39.56, 41.33, 31.56, 46.00, 20.89, 45.33);
|
||||
infinityBody.curveTo(10.44, 44.67, 0.67, 34.89, 0.00, 24.44);
|
||||
infinityBody.curveTo(-0.89, 11.11, 9.33, 0.22, 23.33, 0.00);
|
||||
infinityBody.curveTo(32.67, 0.00, 40.00, 4.67, 45.78, 12.00);
|
||||
infinityBody.curveTo(46.44, 12.89, 47.11, 13.56, 47.78, 14.67);
|
||||
infinityBody.closePath();
|
||||
|
||||
// Left hole cutout
|
||||
infinityBody.moveTo(24.44, 7.56);
|
||||
infinityBody.curveTo(14.67, 7.56, 7.56, 13.78, 6.89, 21.78);
|
||||
infinityBody.curveTo(6.22, 28.89, 11.78, 36.00, 19.56, 37.78);
|
||||
infinityBody.curveTo(27.33, 39.33, 33.33, 36.22, 38.00, 30.67);
|
||||
infinityBody.curveTo(45.11, 22.22, 44.89, 23.56, 38.00, 15.11);
|
||||
infinityBody.curveTo(34.22, 10.44, 29.11, 7.78, 24.22, 7.56);
|
||||
infinityBody.closePath();
|
||||
|
||||
// Right hole cutout
|
||||
infinityBody.moveTo(72.67, 7.33);
|
||||
infinityBody.curveTo(71.56, 7.33, 70.67, 7.33, 69.78, 7.33);
|
||||
infinityBody.curveTo(61.56, 8.89, 56.44, 14.44, 52.44, 21.56);
|
||||
infinityBody.curveTo(52.22, 22.00, 52.22, 23.11, 52.44, 23.56);
|
||||
infinityBody.curveTo(56.00, 29.56, 60.22, 34.89, 67.33, 37.11);
|
||||
infinityBody.curveTo(74.00, 39.33, 80.00, 37.56, 84.67, 32.22);
|
||||
infinityBody.curveTo(88.89, 27.56, 89.78, 22.00, 87.11, 16.44);
|
||||
infinityBody.curveTo(84.22, 10.44, 78.89, 7.78, 72.67, 7.33);
|
||||
infinityBody.lineTo(72.67, 7.33);
|
||||
infinityBody.closePath();
|
||||
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fill(infinityBody);
|
||||
|
||||
// Minus sign in left loop
|
||||
Path2D.Double minus = new Path2D.Double();
|
||||
minus.moveTo(31.33, 24.44);
|
||||
minus.curveTo(31.33, 24.44, 31.33, 24.89, 30.89, 24.89);
|
||||
minus.lineTo(16.44, 24.89);
|
||||
minus.curveTo(16.44, 24.89, 16.00, 24.89, 16.00, 24.44);
|
||||
minus.lineTo(16.00, 20.00);
|
||||
minus.curveTo(16.00, 20.00, 16.00, 19.56, 16.44, 19.56);
|
||||
minus.lineTo(30.89, 19.56);
|
||||
minus.curveTo(30.89, 19.56, 31.33, 19.56, 31.33, 20.00);
|
||||
minus.lineTo(31.33, 24.44);
|
||||
minus.closePath();
|
||||
g2d.fill(minus);
|
||||
|
||||
// Plus sign in right loop
|
||||
Path2D.Double plus = new Path2D.Double();
|
||||
plus.moveTo(78.89, 20.22);
|
||||
plus.curveTo(78.89, 20.22, 78.89, 19.78, 78.44, 19.78);
|
||||
plus.lineTo(74.22, 19.78);
|
||||
plus.curveTo(74.22, 19.78, 73.78, 19.78, 73.78, 19.33);
|
||||
plus.lineTo(73.78, 15.11);
|
||||
plus.curveTo(73.78, 15.11, 73.78, 14.67, 73.33, 14.67);
|
||||
plus.lineTo(68.89, 14.67);
|
||||
plus.curveTo(68.89, 14.67, 68.44, 14.67, 68.44, 15.11);
|
||||
plus.lineTo(68.44, 19.33);
|
||||
plus.curveTo(68.44, 19.33, 68.44, 19.78, 68.00, 19.78);
|
||||
plus.lineTo(63.78, 19.78);
|
||||
plus.curveTo(63.78, 19.78, 63.33, 19.78, 63.33, 20.22);
|
||||
plus.lineTo(63.33, 24.67);
|
||||
plus.curveTo(63.33, 24.67, 63.33, 25.11, 63.78, 25.11);
|
||||
plus.lineTo(68.00, 25.11);
|
||||
plus.curveTo(68.00, 25.11, 68.44, 25.11, 68.44, 25.56);
|
||||
plus.lineTo(68.44, 29.78);
|
||||
plus.curveTo(68.44, 29.78, 68.44, 30.22, 68.89, 30.22);
|
||||
plus.lineTo(73.33, 30.22);
|
||||
plus.curveTo(73.33, 30.22, 73.78, 30.22, 73.78, 29.78);
|
||||
plus.lineTo(73.78, 25.56);
|
||||
plus.curveTo(73.78, 25.56, 73.78, 25.11, 74.22, 25.11);
|
||||
plus.lineTo(78.44, 25.11);
|
||||
plus.curveTo(78.44, 25.11, 78.89, 25.11, 78.89, 24.67);
|
||||
plus.lineTo(78.89, 20.22);
|
||||
plus.closePath();
|
||||
g2d.fill(plus);
|
||||
|
||||
g2d.setTransform(oldTx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to draw the Raspberry Pi logo in white using vector paths.
|
||||
* Native bounding box is 72.51 x 92.60 px.
|
||||
*
|
||||
* @param g2d Graphics2D context
|
||||
* @param x Top-left X coordinate
|
||||
* @param y Top-left Y coordinate
|
||||
* @param size Target height in pixels
|
||||
*/
|
||||
public static void drawRaspberryPiLogo(Graphics2D g2d, double x, double y, double size) {
|
||||
AffineTransform oldTx = g2d.getTransform();
|
||||
g2d.translate(x, y);
|
||||
double scale = size / 92.604;
|
||||
g2d.scale(scale, scale);
|
||||
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fill(RASPBERRY_PI_LOGO_AREA);
|
||||
g2d.setTransform(oldTx);
|
||||
}
|
||||
|
||||
private static Area createRaspberryPiLogoArea() {
|
||||
Path2D.Double p0 = new Path2D.Double(Path2D.WIND_NON_ZERO);
|
||||
p0.moveTo(19.6295, 0.0014);
|
||||
p0.curveTo(19.1612, 0.0159, 18.6569, 0.1889, 18.0849, 0.6402);
|
||||
p0.curveTo(16.6837, 0.0999, 15.3251, -0.0878, 14.1102, 1.0122);
|
||||
p0.curveTo(12.2342, 0.7689, 11.6245, 1.2710, 11.1626, 1.8572);
|
||||
p0.curveTo(10.7507, 1.8487, 8.0807, 1.4339, 6.8564, 3.2603);
|
||||
p0.curveTo(3.7794, 2.8963, 2.8070, 5.0701, 3.9089, 7.0973);
|
||||
p0.curveTo(3.2805, 8.0702, 2.6293, 9.0313, 4.0989, 10.8859);
|
||||
p0.curveTo(3.5790, 11.9189, 3.9013, 13.0394, 5.1259, 14.3954);
|
||||
p0.curveTo(4.8027, 15.8475, 5.4379, 16.8720, 6.5775, 17.6704);
|
||||
p0.curveTo(6.3644, 19.6575, 8.3997, 20.8128, 9.0075, 21.2245);
|
||||
p0.curveTo(9.2408, 22.3823, 9.7272, 23.4749, 12.0520, 24.0790);
|
||||
p0.curveTo(12.4354, 25.8045, 13.8326, 26.1024, 15.1856, 26.4646);
|
||||
p0.curveTo(10.7138, 29.0639, 6.8789, 32.4840, 6.9049, 40.8755);
|
||||
p0.lineTo(6.2500, 42.0440);
|
||||
p0.curveTo(1.1224, 45.1622, -3.4908, 55.1843, 3.7230, 63.3304);
|
||||
p0.curveTo(4.1942, 65.8804, 4.9845, 67.7119, 5.6881, 69.7390);
|
||||
p0.curveTo(6.7405, 77.9072, 13.6088, 81.7320, 15.4203, 82.1843);
|
||||
p0.curveTo(18.0749, 84.2063, 20.9021, 86.1249, 24.7280, 87.4689);
|
||||
p0.curveTo(28.3346, 91.1885, 32.2420, 92.6062, 36.1706, 92.6039);
|
||||
p0.curveTo(36.2282, 92.6039, 36.2868, 92.6045, 36.3445, 92.6039);
|
||||
p0.curveTo(40.2731, 92.6063, 44.1803, 91.1887, 47.7870, 87.4689);
|
||||
p0.curveTo(51.6128, 86.1249, 54.4401, 84.2063, 57.0947, 82.1843);
|
||||
p0.curveTo(58.9062, 81.7319, 65.7745, 77.9072, 66.8269, 69.7390);
|
||||
p0.curveTo(67.5305, 67.7119, 68.3208, 65.8803, 68.7920, 63.3304);
|
||||
p0.curveTo(76.0057, 55.1837, 71.3926, 45.1609, 66.2650, 42.0427);
|
||||
p0.lineTo(65.6090, 40.8744);
|
||||
p0.curveTo(65.6350, 32.4834, 61.8002, 29.0634, 57.3284, 26.4634);
|
||||
p0.curveTo(58.6814, 26.1014, 60.0786, 25.8034, 60.4619, 24.0779);
|
||||
p0.curveTo(62.7867, 23.4737, 63.2732, 22.3811, 63.5065, 21.2234);
|
||||
p0.curveTo(64.1143, 20.8118, 66.1495, 19.6564, 65.9365, 17.6693);
|
||||
p0.curveTo(67.0759, 16.8708, 67.7113, 15.8463, 67.3880, 14.3943);
|
||||
p0.curveTo(68.6127, 13.0383, 68.9350, 11.9177, 68.4150, 10.8847);
|
||||
p0.curveTo(69.8849, 9.0309, 69.2328, 8.0698, 68.6052, 7.0970);
|
||||
p0.curveTo(69.7063, 5.0698, 68.7346, 2.8959, 65.6566, 3.2600);
|
||||
p0.curveTo(64.4326, 1.4335, 61.7634, 1.8484, 61.3506, 1.8569);
|
||||
p0.curveTo(60.8887, 1.2707, 60.2793, 0.7685, 58.4032, 1.0119);
|
||||
p0.curveTo(57.1883, -0.0882, 55.8298, 0.0995, 54.4285, 0.6399);
|
||||
p0.curveTo(52.7646, -0.6731, 51.6635, 0.3794, 50.4059, 0.7773);
|
||||
p0.curveTo(48.3914, 0.1191, 47.9309, 1.0207, 46.9410, 1.3879);
|
||||
p0.curveTo(44.7440, 0.9236, 44.0764, 1.9344, 43.0232, 3.0012);
|
||||
p0.lineTo(41.7982, 2.9769);
|
||||
p0.curveTo(38.4844, 4.9297, 36.8382, 8.9061, 36.2548, 10.9503);
|
||||
p0.curveTo(35.6710, 8.9057, 34.0286, 4.9293, 30.7155, 2.9769);
|
||||
p0.lineTo(29.4904, 3.0012);
|
||||
p0.curveTo(28.4359, 1.9344, 27.7683, 0.9236, 25.5714, 1.3879);
|
||||
p0.curveTo(24.5815, 1.0207, 24.1222, 0.1191, 22.1064, 0.7773);
|
||||
p0.curveTo(21.2810, 0.5162, 20.5218, -0.0267, 19.6279, 0.0010);
|
||||
p0.closePath();
|
||||
|
||||
Path2D.Double p1 = new Path2D.Double(Path2D.WIND_NON_ZERO);
|
||||
p1.moveTo(13.0322, 8.5925);
|
||||
p1.curveTo(21.8236, 13.1250, 26.9346, 16.7915, 29.7345, 19.9142);
|
||||
p1.curveTo(28.3006, 25.6612, 20.8204, 25.9235, 18.0852, 25.7623);
|
||||
p1.curveTo(18.6452, 25.5016, 19.1125, 25.1894, 19.2782, 24.7097);
|
||||
p1.curveTo(18.5919, 24.2219, 16.1583, 24.6583, 14.4594, 23.7037);
|
||||
p1.curveTo(15.1120, 23.5686, 15.4173, 23.4369, 15.7225, 22.9552);
|
||||
p1.curveTo(14.1174, 22.4433, 12.3885, 22.0022, 11.3715, 21.1541);
|
||||
p1.curveTo(11.9203, 21.1609, 12.4327, 21.2768, 13.1493, 20.7798);
|
||||
p1.curveTo(11.7116, 20.0050, 10.1775, 19.3910, 8.9856, 18.2068);
|
||||
p1.curveTo(9.7289, 18.1885, 10.5303, 18.1994, 10.7633, 17.9260);
|
||||
p1.curveTo(9.4475, 17.1108, 8.3373, 16.2042, 7.4183, 15.2126);
|
||||
p1.curveTo(8.4585, 15.3381, 8.8977, 15.2300, 9.1493, 15.0488);
|
||||
p1.curveTo(8.1546, 14.0300, 6.8956, 13.1698, 6.2954, 11.9143);
|
||||
p1.curveTo(7.0678, 12.1805, 7.7744, 12.2825, 8.2838, 11.8909);
|
||||
p1.curveTo(7.9458, 11.1284, 6.4976, 10.6787, 5.6638, 8.8967);
|
||||
p1.curveTo(6.4770, 8.9756, 7.3394, 9.0741, 7.5119, 8.8967);
|
||||
p1.curveTo(7.1345, 7.3591, 6.4869, 6.4947, 5.8518, 5.5991);
|
||||
p1.curveTo(7.5920, 5.5732, 10.2286, 5.6058, 10.1092, 5.4587);
|
||||
p1.lineTo(9.0332, 4.3593);
|
||||
p1.curveTo(10.7330, 3.9016, 12.4723, 4.4328, 13.7350, 4.8271);
|
||||
p1.curveTo(14.3020, 4.3797, 13.7250, 3.8141, 13.0332, 3.2365);
|
||||
p1.curveTo(14.4778, 3.4293, 15.7832, 3.7615, 16.9630, 4.2189);
|
||||
p1.curveTo(17.5935, 3.6497, 16.5537, 3.0805, 16.0507, 2.5113);
|
||||
p1.curveTo(18.2825, 2.9347, 19.2281, 3.5297, 20.1677, 4.1253);
|
||||
p1.curveTo(20.8496, 3.4719, 20.2068, 2.9165, 19.7467, 2.3476);
|
||||
p1.curveTo(21.4295, 2.9709, 22.2962, 3.7756, 23.2088, 4.5698);
|
||||
p1.curveTo(23.5182, 4.1523, 23.9948, 3.8463, 23.4193, 2.8388);
|
||||
p1.curveTo(24.6141, 3.5275, 25.5139, 4.3390, 26.1796, 5.2482);
|
||||
p1.curveTo(26.9189, 4.7775, 26.6200, 4.1338, 26.6241, 3.5406);
|
||||
p1.curveTo(27.8657, 4.5506, 28.6537, 5.6255, 29.6183, 6.6751);
|
||||
p1.curveTo(29.8126, 6.5336, 29.9827, 6.0538, 30.1329, 5.2949);
|
||||
p1.curveTo(33.0952, 8.1688, 37.2809, 15.4074, 31.2089, 18.2774);
|
||||
p1.curveTo(26.0413, 14.0154, 19.8697, 10.9176, 13.0304, 8.5937);
|
||||
p1.closePath();
|
||||
p1.moveTo(59.6793, 8.5925);
|
||||
p1.curveTo(50.8889, 13.1254, 45.7783, 16.7909, 42.9783, 19.9142);
|
||||
p1.curveTo(44.4122, 25.6612, 51.8924, 25.9235, 54.6276, 25.7623);
|
||||
p1.curveTo(54.0676, 25.5016, 53.6003, 25.1894, 53.4346, 24.7097);
|
||||
p1.curveTo(54.1210, 24.2219, 56.5545, 24.6583, 58.2535, 23.7037);
|
||||
p1.curveTo(57.6008, 23.5686, 57.2955, 23.4369, 56.9903, 22.9552);
|
||||
p1.curveTo(58.5954, 22.4433, 60.3244, 22.0022, 61.3413, 21.1541);
|
||||
p1.curveTo(60.7925, 21.1609, 60.2801, 21.2768, 59.5635, 20.7798);
|
||||
p1.curveTo(61.0012, 20.0050, 62.5353, 19.3910, 63.7272, 18.2068);
|
||||
p1.curveTo(62.9839, 18.1885, 62.1825, 18.1994, 61.9495, 17.9260);
|
||||
p1.curveTo(63.2653, 17.1108, 64.3755, 16.2042, 65.2946, 15.2126);
|
||||
p1.curveTo(64.2543, 15.3381, 63.8151, 15.2300, 63.5636, 15.0488);
|
||||
p1.curveTo(64.5582, 14.0300, 65.8172, 13.1698, 66.4174, 11.9143);
|
||||
p1.curveTo(65.6450, 12.1805, 64.9384, 12.2825, 64.4290, 11.8909);
|
||||
p1.curveTo(64.7670, 11.1284, 66.2152, 10.6787, 67.0490, 8.8967);
|
||||
p1.curveTo(66.2358, 8.9756, 65.3734, 9.0741, 65.2010, 8.8967);
|
||||
p1.curveTo(65.5790, 7.3585, 66.2266, 6.4940, 66.8617, 5.5984);
|
||||
p1.curveTo(65.1215, 5.5726, 62.4849, 5.6052, 62.6043, 5.4581);
|
||||
p1.lineTo(63.6803, 4.3587);
|
||||
p1.curveTo(61.9805, 3.9010, 60.2411, 4.4322, 58.9785, 4.8265);
|
||||
p1.curveTo(58.4115, 4.3791, 58.9885, 3.8135, 59.6802, 3.2358);
|
||||
p1.curveTo(58.2357, 3.4287, 56.9302, 3.7608, 55.7504, 4.2183);
|
||||
p1.curveTo(55.1200, 3.6491, 56.1598, 3.0799, 56.6627, 2.5107);
|
||||
p1.curveTo(54.4310, 2.9341, 53.4854, 3.5290, 52.5457, 4.1247);
|
||||
p1.curveTo(51.8639, 3.4713, 52.5067, 2.9159, 52.9667, 2.3470);
|
||||
p1.curveTo(51.2840, 2.9703, 50.4172, 3.7750, 49.5047, 4.5692);
|
||||
p1.curveTo(49.1953, 4.1517, 48.7187, 3.8456, 49.2941, 2.8382);
|
||||
p1.curveTo(48.0994, 3.5268, 47.1995, 4.3384, 46.5338, 5.2476);
|
||||
p1.curveTo(45.7946, 4.7769, 46.0934, 4.1332, 46.0894, 3.5399);
|
||||
p1.curveTo(44.8477, 4.5499, 44.0597, 5.6249, 43.0952, 6.6744);
|
||||
p1.curveTo(42.9009, 6.5330, 42.7307, 6.0532, 42.5805, 5.2943);
|
||||
p1.curveTo(39.6183, 8.1682, 35.4325, 15.4067, 41.5045, 18.2768);
|
||||
p1.curveTo(46.6695, 14.0139, 52.8407, 10.9163, 59.6805, 8.5924);
|
||||
p1.closePath();
|
||||
|
||||
Path2D.Double p2 = new Path2D.Double(Path2D.WIND_NON_ZERO);
|
||||
p2.moveTo(47.0024, 67.1140);
|
||||
p2.curveTo(47.0188, 70.5889, 45.0112, 73.8170, 41.7504, 75.5588);
|
||||
p2.curveTo(38.4896, 77.3006, 34.4542, 77.3006, 31.1934, 75.5588);
|
||||
p2.curveTo(27.9326, 73.8170, 25.9250, 70.5889, 25.9414, 67.1140);
|
||||
p2.curveTo(25.9250, 63.6391, 27.9326, 60.4110, 31.1934, 58.6692);
|
||||
p2.curveTo(34.4542, 56.9274, 38.4896, 56.9274, 41.7504, 58.6692);
|
||||
p2.curveTo(45.0112, 60.4110, 47.0188, 63.6391, 47.0024, 67.1140);
|
||||
p2.closePath();
|
||||
p2.moveTo(30.3786, 39.3746);
|
||||
p2.curveTo(33.2055, 41.2176, 34.6522, 44.7077, 34.1649, 48.5090);
|
||||
p2.curveTo(33.6776, 52.3102, 31.3275, 55.8665, 28.0144, 57.8164);
|
||||
p2.curveTo(24.7013, 59.7663, 20.9097, 59.8245, 18.0912, 57.9688);
|
||||
p2.curveTo(15.2643, 56.1258, 13.8176, 52.6357, 14.3049, 48.8344);
|
||||
p2.curveTo(14.7922, 45.0332, 17.1423, 41.4769, 20.4554, 39.5270);
|
||||
p2.curveTo(23.7685, 37.5771, 27.5601, 37.5189, 30.3786, 39.3746);
|
||||
p2.closePath();
|
||||
|
||||
Path2D.Double p3 = new Path2D.Double(Path2D.WIND_NON_ZERO);
|
||||
p3.moveTo(42.1557, 38.8570);
|
||||
p3.curveTo(37.8215, 41.6966, 37.0284, 48.2301, 40.3987, 53.3303);
|
||||
p3.curveTo(43.7691, 58.4306, 50.1089, 60.2909, 54.4431, 57.4513);
|
||||
p3.curveTo(58.7773, 54.6117, 59.5704, 48.0782, 56.2001, 42.9780);
|
||||
p3.curveTo(52.8297, 37.8777, 46.4899, 36.0174, 42.1557, 38.8570);
|
||||
p3.closePath();
|
||||
p3.moveTo(8.5710, 44.0484);
|
||||
p3.curveTo(13.2819, 42.7856, 10.1614, 63.5377, 6.3284, 61.8350);
|
||||
p3.curveTo(2.1120, 58.4438, 0.7540, 48.5122, 8.5710, 44.0484);
|
||||
p3.closePath();
|
||||
|
||||
Path2D.Double p4 = new Path2D.Double(Path2D.WIND_NON_ZERO);
|
||||
p4.moveTo(63.0123, 43.7896);
|
||||
p4.curveTo(58.3009, 42.5272, 61.4219, 63.2802, 65.2549, 61.5775);
|
||||
p4.curveTo(69.4712, 58.1859, 70.8292, 48.2535, 63.0123, 43.7896);
|
||||
p4.closePath();
|
||||
p4.moveTo(47.0060, 28.3359);
|
||||
p4.curveTo(55.1360, 26.9631, 61.9009, 31.7933, 61.6279, 40.6091);
|
||||
p4.curveTo(61.3605, 43.9888, 44.0109, 28.8391, 47.0061, 28.3359);
|
||||
p4.closePath();
|
||||
p4.moveTo(24.5436, 28.0771);
|
||||
p4.curveTo(16.4130, 26.7043, 9.6488, 31.5356, 9.9218, 40.3506);
|
||||
p4.curveTo(10.1892, 43.7301, 27.5388, 28.5804, 24.5436, 28.0771);
|
||||
p4.closePath();
|
||||
p4.moveTo(36.2218, 26.0212);
|
||||
p4.curveTo(31.3695, 25.8950, 26.7127, 29.6225, 26.7014, 31.7845);
|
||||
p4.curveTo(26.6879, 34.4115, 30.5378, 37.1013, 36.2548, 37.1695);
|
||||
p4.curveTo(42.0931, 37.2113, 45.8185, 35.0165, 45.8372, 32.3055);
|
||||
p4.curveTo(45.8585, 29.2338, 40.5274, 25.9737, 36.2217, 26.0211);
|
||||
p4.closePath();
|
||||
p4.moveTo(36.5181, 79.8920);
|
||||
p4.curveTo(40.7486, 79.7073, 46.4253, 81.2546, 46.4364, 83.3072);
|
||||
p4.curveTo(46.5066, 85.3003, 41.2882, 89.8037, 36.2375, 89.7166);
|
||||
p4.curveTo(31.0069, 89.9423, 25.8778, 85.4319, 25.9449, 83.8686);
|
||||
p4.curveTo(25.8666, 81.5764, 32.3140, 79.7867, 36.5181, 79.8919);
|
||||
p4.closePath();
|
||||
p4.moveTo(20.8923, 67.7271);
|
||||
p4.curveTo(23.9043, 71.3559, 25.2773, 77.7311, 22.7637, 79.6104);
|
||||
p4.curveTo(20.3857, 81.0450, 14.6109, 80.4542, 10.5063, 74.5576);
|
||||
p4.curveTo(7.7381, 69.6096, 8.0949, 64.5746, 10.0385, 63.0956);
|
||||
p4.curveTo(12.9450, 61.3250, 17.4358, 63.7165, 20.8925, 67.7271);
|
||||
p4.closePath();
|
||||
p4.moveTo(51.5410, 66.5770);
|
||||
p4.curveTo(48.2822, 70.3940, 46.4676, 77.3560, 48.8449, 79.5982);
|
||||
p4.curveTo(51.1179, 81.3402, 57.2199, 81.0967, 61.7272, 74.8425);
|
||||
p4.curveTo(65.0002, 70.6421, 63.9035, 63.6272, 62.0340, 61.7643);
|
||||
p4.curveTo(59.2569, 59.6163, 55.2701, 62.3653, 51.5410, 66.5759);
|
||||
p4.closePath();
|
||||
|
||||
Area area = new Area(p0);
|
||||
area.subtract(new Area(p1));
|
||||
area.subtract(new Area(p2));
|
||||
area.subtract(new Area(p3));
|
||||
area.subtract(new Area(p4));
|
||||
|
||||
return area;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
/*
|
||||
*
|
||||
* DIY Layout Creator (DIYLC).
|
||||
* Copyright (c) 2009-2025 held jointly by the individual authors.
|
||||
*
|
||||
* This file is part of DIYLC.
|
||||
*
|
||||
* DIYLC is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* DIYLC is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.diylc.components;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.awt.StringUtils;
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.common.HorizontalAlignment;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.common.VerticalAlignment;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.diylc.utils.Constants;
|
||||
|
||||
/**
|
||||
* Painters for the discrete parts populated onto a maker board -- chips, connectors, antennas,
|
||||
* buttons and mounting holes. They are pure functions of the geometry they are handed, drawn in
|
||||
* whatever coordinate space {@code g2d} is currently in, so a board calls them while its own
|
||||
* rotation is still applied.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class MakerBoardPainter {
|
||||
|
||||
private MakerBoardPainter() {}
|
||||
|
||||
public static void drawMountingHole(Graphics2D g2d, double cx, double cy, double diameter) {
|
||||
g2d.setColor(Constants.CANVAS_COLOR);
|
||||
g2d.fill(new Ellipse2D.Double(cx - diameter / 2.0, cy - diameter / 2.0, diameter, diameter));
|
||||
g2d.setColor(Color.DARK_GRAY);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(new Ellipse2D.Double(cx - diameter / 2.0, cy - diameter / 2.0, diameter, diameter));
|
||||
}
|
||||
|
||||
public static void drawChip(Graphics2D g2d, double x, double y, double w, double h, String label) {
|
||||
g2d.setColor(AbstractMakerBoard.IC_BODY_COLOR);
|
||||
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 4, 4));
|
||||
g2d.setColor(AbstractMakerBoard.IC_BORDER_COLOR);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(new RoundRectangle2D.Double(x, y, w, h, 4, 4));
|
||||
|
||||
// pin 1 dot
|
||||
g2d.setColor(AbstractMakerBoard.PIN_MARKER_COLOR);
|
||||
g2d.fill(new Ellipse2D.Double(x + 3, y + 3, 3, 3));
|
||||
|
||||
if (label != null && !label.isEmpty() && w > 20 && h > 10) {
|
||||
g2d.setColor(AbstractMakerBoard.IC_TEXT_COLOR);
|
||||
g2d.setFont(AbstractMakerBoard.SILK_FONT_SMALL);
|
||||
StringUtils.drawCenteredText(g2d, label, x + w / 2.0, y + h / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
/** An FPC / ribbon cable connector, such as a MIPI CSI/DSI or PCIe socket. */
|
||||
public static void drawFpcConnector(Graphics2D g2d, double x, double y, double w, double h, boolean vertical, String label) {
|
||||
g2d.setColor(AbstractMakerBoard.IC_BODY_COLOR);
|
||||
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 2, 2));
|
||||
g2d.setColor(AbstractMakerBoard.IC_BORDER_COLOR);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(new RoundRectangle2D.Double(x, y, w, h, 2, 2));
|
||||
|
||||
// Inner slot / latch bar
|
||||
g2d.setColor(AbstractMakerBoard.HEADER_BODY_COLOR.darker());
|
||||
if (vertical) {
|
||||
double slotW = Math.max(1.5, w * 0.35);
|
||||
double slotH = Math.max(2.0, h - 4);
|
||||
g2d.fill(new Rectangle2D.Double(x + (w - slotW) / 2.0, y + 2, slotW, slotH));
|
||||
} else {
|
||||
double slotW = Math.max(2.0, w - 4);
|
||||
double slotH = Math.max(1.5, h * 0.35);
|
||||
g2d.fill(new Rectangle2D.Double(x + 2, y + (h - slotH) / 2.0, slotW, slotH));
|
||||
}
|
||||
|
||||
if (label != null && !label.isEmpty()) {
|
||||
g2d.setColor(AbstractMakerBoard.SILK_COLOR);
|
||||
g2d.setFont(AbstractMakerBoard.SILK_FONT_SMALL);
|
||||
if (w >= 20 && h >= 10) {
|
||||
StringUtils.drawCenteredText(g2d, label, x + w / 2.0, y + h / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawMicroUsb(Graphics2D g2d, double x, double y, double w, double h, String label) {
|
||||
drawMetalConnector(g2d, x, y, w, h, label);
|
||||
}
|
||||
|
||||
public static void drawUsbC(Graphics2D g2d, double x, double y, double w, double h, String label) {
|
||||
drawMetalConnector(g2d, x, y, w, h, label);
|
||||
}
|
||||
|
||||
public static void drawUsbA(Graphics2D g2d, double x, double y, double w, double h, String label) {
|
||||
drawMetalConnector(g2d, x, y, w, h, label);
|
||||
}
|
||||
|
||||
public static void drawUsbB(Graphics2D g2d, double x, double y, double w, double h, String label) {
|
||||
drawMetalConnector(g2d, x, y, w, h, label);
|
||||
}
|
||||
|
||||
/** A metal shield or connector body: an RF can, an SD card slot, an HDMI socket. */
|
||||
public static void drawMetalConnector(Graphics2D g2d, double x, double y, double w, double h, String label) {
|
||||
g2d.setColor(AbstractMakerBoard.USB_METAL_COLOR);
|
||||
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 3, 3));
|
||||
g2d.setColor(AbstractMakerBoard.METAL_SHIELD_BORDER);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(new RoundRectangle2D.Double(x, y, w, h, 3, 3));
|
||||
if (label != null && !label.isEmpty()) {
|
||||
g2d.setColor(AbstractMakerBoard.METAL_LABEL_COLOR);
|
||||
g2d.setFont(AbstractMakerBoard.SILK_FONT_SMALL);
|
||||
StringUtils.drawCenteredText(g2d, label, x + w / 2.0, y + h / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ESP-style PCB meander antenna over a dark substrate rectangle.
|
||||
*
|
||||
* @param g2d Graphics2D context
|
||||
* @param x top-left X coordinate of the antenna substrate rectangle
|
||||
* @param y top-left Y coordinate of the antenna substrate rectangle
|
||||
* @param width width of the antenna substrate rectangle
|
||||
* @param height height of the antenna substrate rectangle
|
||||
*/
|
||||
public static void drawPcbAntenna(Graphics2D g2d, double x, double y, double width, double height) {
|
||||
g2d.setColor(AbstractMakerBoard.ANTENNA_BG_COLOR);
|
||||
g2d.fill(new Rectangle2D.Double(x, y, width, height));
|
||||
|
||||
g2d.setColor(AbstractMakerBoard.ANTENNA_COLOR);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
|
||||
Path2D.Double antPath = new Path2D.Double();
|
||||
|
||||
if (width >= height) {
|
||||
double antPadX = x + new Size(1.5d, SizeUnit.mm).convertToPixels();
|
||||
double antPadW = width - new Size(3.0d, SizeUnit.mm).convertToPixels();
|
||||
double traceH = new Size(4.1d, SizeUnit.mm).convertToPixels();
|
||||
double marginY = height > traceH ? (height - traceH) / 2.0 : height * 0.15;
|
||||
double antTopY = y + marginY;
|
||||
double antMidY = y + height - marginY;
|
||||
antPath.moveTo(antPadX, antMidY);
|
||||
antPath.lineTo(antPadX, antTopY);
|
||||
antPath.lineTo(antPadX + antPadW * 0.25, antTopY);
|
||||
antPath.lineTo(antPadX + antPadW * 0.25, antMidY);
|
||||
antPath.lineTo(antPadX + antPadW * 0.50, antMidY);
|
||||
antPath.lineTo(antPadX + antPadW * 0.50, antTopY);
|
||||
antPath.lineTo(antPadX + antPadW * 0.75, antTopY);
|
||||
antPath.lineTo(antPadX + antPadW * 0.75, antMidY);
|
||||
antPath.lineTo(antPadX + antPadW, antMidY);
|
||||
antPath.lineTo(antPadX + antPadW, antTopY);
|
||||
} else {
|
||||
double antPadY = y + new Size(1.5d, SizeUnit.mm).convertToPixels();
|
||||
double antPadH = height - new Size(3.0d, SizeUnit.mm).convertToPixels();
|
||||
double traceW = new Size(4.1d, SizeUnit.mm).convertToPixels();
|
||||
double marginX = width > traceW ? (width - traceW) / 2.0 : width * 0.15;
|
||||
double antLeftX = x + marginX;
|
||||
double antRightX = x + width - marginX;
|
||||
antPath.moveTo(antRightX, antPadY);
|
||||
antPath.lineTo(antLeftX, antPadY);
|
||||
antPath.lineTo(antLeftX, antPadY + antPadH * 0.25);
|
||||
antPath.lineTo(antRightX, antPadY + antPadH * 0.25);
|
||||
antPath.lineTo(antRightX, antPadY + antPadH * 0.50);
|
||||
antPath.lineTo(antLeftX, antPadY + antPadH * 0.50);
|
||||
antPath.lineTo(antLeftX, antPadY + antPadH * 0.75);
|
||||
antPath.lineTo(antRightX, antPadY + antPadH * 0.75);
|
||||
antPath.lineTo(antRightX, antPadY + antPadH);
|
||||
antPath.lineTo(antLeftX, antPadY + antPadH);
|
||||
}
|
||||
g2d.draw(antPath);
|
||||
}
|
||||
|
||||
/** As above, at the default 15.0 x 7.0 mm. */
|
||||
public static void drawPcbAntenna(Graphics2D g2d, double x, double y) {
|
||||
drawPcbAntenna(g2d, x, y, AbstractMakerBoard.ANTENNA_WIDTH.convertToPixels(), AbstractMakerBoard.ANTENNA_LENGTH.convertToPixels());
|
||||
}
|
||||
|
||||
/**
|
||||
* An SMD tactile push button: housing plus circular actuator.
|
||||
*
|
||||
* @param g2d Graphics2D context
|
||||
* @param x Top-left X coordinate of the button body
|
||||
* @param y Top-left Y coordinate of the button body
|
||||
* @param w Width of the button body
|
||||
* @param h Height of the button body
|
||||
*/
|
||||
public static void drawButton(Graphics2D g2d, double x, double y, double w, double h) {
|
||||
g2d.setColor(AbstractMakerBoard.BUTTON_BODY_COLOR);
|
||||
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 2, 2));
|
||||
g2d.setColor(AbstractMakerBoard.BUTTON_BORDER_COLOR);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(new RoundRectangle2D.Double(x, y, w, h, 2, 2));
|
||||
|
||||
double actuatorD = Math.min(w, h) * 0.55;
|
||||
g2d.setColor(AbstractMakerBoard.BUTTON_ACTUATOR_COLOR);
|
||||
g2d.fill(new Ellipse2D.Double(x + (w - actuatorD) / 2.0, y + (h - actuatorD) / 2.0, actuatorD, actuatorD));
|
||||
}
|
||||
|
||||
/**
|
||||
* An addressable RGB LED in a 5050 package: the white body, the milky phosphor lens over it and
|
||||
* the driver die visible through the lens. Shared by every WS2812B part.
|
||||
*
|
||||
* @param g2d Graphics2D context
|
||||
* @param cx centre X coordinate of the package
|
||||
* @param cy centre Y coordinate of the package
|
||||
* @param size edge length of the square package; lens and die scale with it
|
||||
*/
|
||||
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size) {
|
||||
drawAddressableLed(g2d, cx, cy, size, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws one 5050 addressable RGB package. A {@code litColor} shows the LED emitting that colour;
|
||||
* {@code null} draws it dark, which is how an unpowered part looks sitting on a layout.
|
||||
*/
|
||||
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
|
||||
Color litColor) {
|
||||
drawAddressableLed(g2d, cx, cy, size, litColor, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* As above, turned by {@code rotation} radians about its own centre, for parts laid out on a
|
||||
* circle and mounted facing outwards. The package is square, so the turn only tells modulo a
|
||||
* quarter turn -- still enough to distinguish a radial arrangement from an axis-aligned one.
|
||||
*/
|
||||
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
|
||||
Color litColor, double rotation) {
|
||||
drawAddressableLed(g2d, cx, cy, size, litColor, rotation, AbstractMakerBoard.LedType.RGB);
|
||||
}
|
||||
|
||||
/**
|
||||
* As above, for a package other than the plain RGB one. An RGBW is told from an RGB on sight
|
||||
* only by the white die beside the colour dies, so that is what is drawn; WWA has the same
|
||||
* three-die layout and is told apart by what it emits, which is the caller's palette.
|
||||
*/
|
||||
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
|
||||
Color litColor, double rotation, AbstractMakerBoard.LedType ledType) {
|
||||
if (rotation != 0) {
|
||||
g2d.rotate(rotation, cx, cy);
|
||||
}
|
||||
|
||||
double half = size / 2.0;
|
||||
|
||||
g2d.setColor(AbstractMakerBoard.RGB_LED_BODY_COLOR);
|
||||
g2d.fill(new RoundRectangle2D.Double(cx - half, cy - half, size, size, 2, 2));
|
||||
g2d.setColor(AbstractMakerBoard.RGB_LED_BODY_BORDER);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(0.5f));
|
||||
g2d.draw(new RoundRectangle2D.Double(cx - half, cy - half, size, size, 2, 2));
|
||||
|
||||
double lensR = half * 0.7;
|
||||
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_LENS_COLOR : litColor);
|
||||
g2d.fill(new Ellipse2D.Double(cx - lensR, cy - lensR, lensR * 2, lensR * 2));
|
||||
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_LENS_BORDER : litColor.darker());
|
||||
g2d.draw(new Ellipse2D.Double(cx - lensR, cy - lensR, lensR * 2, lensR * 2));
|
||||
|
||||
double dieSize = Math.max(2.0, lensR * 0.4);
|
||||
// the colour dies sit centred on their own, and shoulder aside to make room for a white one
|
||||
double dieOffset = ledType != null && ledType.hasWhiteDie() ? dieSize * 0.7 : 0;
|
||||
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_CHIP_COLOR : litColor.brighter());
|
||||
g2d.fill(new Rectangle2D.Double(cx - dieOffset - dieSize / 2.0, cy - dieSize / 2.0, dieSize,
|
||||
dieSize));
|
||||
if (dieOffset > 0) {
|
||||
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_CHIP_COLOR : Color.WHITE);
|
||||
g2d.fill(new Rectangle2D.Double(cx + dieOffset - dieSize / 2.0, cy - dieSize / 2.0, dieSize,
|
||||
dieSize));
|
||||
}
|
||||
|
||||
// turned back rather than saving the transform, so nothing is allocated per LED
|
||||
if (rotation != 0) {
|
||||
g2d.rotate(-rotation, cx, cy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a display's own description on its lit area, wrapped and centred.
|
||||
*
|
||||
* <p>What it prints is what the component already answers for the BOM, so the glass cannot fall
|
||||
* out of step with the part list. Nothing is drawn unless the whole block fits, so a panel too
|
||||
* small for its description shows a bare screen rather than a crop -- the same rule the pin-name
|
||||
* silkscreen follows.
|
||||
*
|
||||
* <p>The area is the part of the panel the text may occupy rather than the panel itself, so a
|
||||
* round display hands over the square inscribed in its glass.
|
||||
*/
|
||||
public static void drawScreenText(Graphics2D g2d, Rectangle2D area, Color inkColor,
|
||||
Display display, String name, String value) {
|
||||
String text = screenText(display, name, value);
|
||||
if (text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
g2d.setFont(AbstractMakerBoard.SILK_FONT);
|
||||
FontMetrics metrics = g2d.getFontMetrics();
|
||||
double margin = AbstractMakerBoard.SCREEN_TEXT_MARGIN.convertToPixels();
|
||||
double maxWidth = area.getWidth() - 2 * margin;
|
||||
double maxHeight = area.getHeight() - 2 * margin;
|
||||
double lineHeight = metrics.getHeight();
|
||||
|
||||
List<String> lines = StringUtils.wrap(text, metrics, (int) maxWidth);
|
||||
if (lines.isEmpty() || lines.size() * lineHeight > maxHeight) {
|
||||
return;
|
||||
}
|
||||
// a single word wider than the area cannot be broken, so the width is checked after wrapping
|
||||
// rather than trusted to it
|
||||
for (String line : lines) {
|
||||
if (metrics.stringWidth(line) > maxWidth) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
g2d.setColor(inkColor);
|
||||
double y = area.getCenterY() - (lines.size() - 1) * lineHeight / 2.0;
|
||||
for (String line : lines) {
|
||||
StringUtils.drawCenteredText(g2d, line, area.getCenterX(), y, HorizontalAlignment.CENTER,
|
||||
VerticalAlignment.CENTER);
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What each {@link Display} setting puts on the glass, or {@code null} for a screen that prints
|
||||
* nothing. The variant string reads "size, interface", and breaking it at the comma gives one
|
||||
* property per line, which keeps the longest line well inside the glass instead of spanning it.
|
||||
*/
|
||||
public static String screenText(Display display, String name, String value) {
|
||||
boolean hasValue = value != null && !value.trim().isEmpty();
|
||||
String variant = hasValue ? value.replace(", ", "\n") : null;
|
||||
|
||||
if (display == null) {
|
||||
return variant;
|
||||
}
|
||||
switch (display) {
|
||||
case NONE:
|
||||
return null;
|
||||
case NAME:
|
||||
return name;
|
||||
case BOTH:
|
||||
return variant == null ? name : name + "\n" + variant;
|
||||
default:
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user