diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index 42f27abf3..4e5d003a7 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -333,6 +333,10 @@ 2025-05-26 00:00:00.000 CET + + NEW_FEATURE + Added 'Drill Hole' component that can be used for board mounting holes, chassis holes, faceplace holes, etc + IMPROVEMENT Simplify zoom in/out keyboard shortcuts to only plus/minus keys and zero numerical key for zoom reset to 100 diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/DrillHole.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/DrillHole.java new file mode 100644 index 000000000..1a3acd742 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/DrillHole.java @@ -0,0 +1,233 @@ +/* + * + * 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 + * . + */ +package org.diylc.components.electromechanical; + +import java.awt.*; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import com.bancika.gerberwriter.GerberFunctions; + +import org.diylc.components.AbstractTransparentComponent; +import org.diylc.components.transform.SimpleComponentTransformer; +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.core.annotations.KeywordPolicy; +import org.diylc.core.annotations.PositiveNonZeroMeasureValidator; +import org.diylc.core.gerber.GerberLayer; +import org.diylc.core.gerber.IGerberComponentCustom; +import org.diylc.core.gerber.IGerberDrawingObserver; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; + +@ComponentDescriptor(name = "Drill Hole", category = "Electromechanical", author = "Branislav Stojkovic", + description = "Drill hole marker for PCB, chassis or faceplate", instanceNamePrefix = "Hole", + zOrder = IDIYComponent.BOARD + 0.1, bomPolicy = BomPolicy.NEVER_SHOW, + keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB", + transformer = SimpleComponentTransformer.class, enableCache = true) +public class DrillHole extends AbstractTransparentComponent + implements IGerberComponentCustom { + + private static final long serialVersionUID = 1L; + + public static Size DIAMETER = new Size(3.0d, SizeUnit.mm); + public static Color COLOR = Color.black; + public static Size STROKE_WIDTH = new Size(1.0d, SizeUnit.px); + public static Size CROSSHAIR_WIDTH = new Size(1.0d, SizeUnit.px); + + private Size diameter = DIAMETER; + private Color color = COLOR; + private Point2D.Double point = new Point2D.Double(0, 0); + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, + Project project, IDrawingObserver drawingObserver, + IGerberDrawingObserver gerberDrawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + double holeDiameter = getDiameter().convertToPixels(); + double borderWidth = STROKE_WIDTH.convertToPixels(); // Use constant stroke width + + g2d.setColor( + componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING + ? SELECTION_COLOR + : color); + + drawingObserver.startTrackingContinuityArea(true); + + // Export to Gerber drill layer + if (gerberDrawingObserver != null) { + GerberLayer drillLayer = GerberLayer.DrillNonPlated; + gerberDrawingObserver.startGerberOutput(drillLayer, GerberFunctions.COMPONENT_DRILL, false); + } + + Composite oldComposite = g2d.getComposite(); + if (getAlpha() < MAX_ALPHA) { + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * getAlpha() / MAX_ALPHA)); + } + + // Draw white circle (hole) + g2d.setColor(Color.white); + g2d.fill(new Ellipse2D.Double(point.getX() - holeDiameter / 2, point.getY() - holeDiameter / 2, + holeDiameter, holeDiameter)); + + // Draw colored border + g2d.setColor(color); + g2d.setStroke(new BasicStroke((float) borderWidth)); + g2d.draw(new Ellipse2D.Double(point.getX() - holeDiameter / 2, point.getY() - holeDiameter / 2, + holeDiameter, holeDiameter)); + + // Draw crosshairs + double crosshairLength = holeDiameter * 0.4; // 40% of diameter for crosshair length (increased from 30%) + double crosshairWidth = CROSSHAIR_WIDTH.convertToPixels(); // Use constant crosshair width + + g2d.setStroke(new BasicStroke((float) crosshairWidth)); + + // Horizontal line + g2d.draw(new Line2D.Double( + point.getX() - crosshairLength / 2, point.getY(), + point.getX() + crosshairLength / 2, point.getY())); + + // Vertical line + g2d.draw(new Line2D.Double( + point.getX(), point.getY() - crosshairLength / 2, + point.getX(), point.getY() + crosshairLength / 2)); + + drawingObserver.stopTracking(); + drawingObserver.stopTrackingContinuityArea(); + + if (gerberDrawingObserver != null) { + gerberDrawingObserver.stopGerberOutput(); + } + + g2d.setComposite(oldComposite); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, + Project project, IDrawingObserver drawingObserver) { + this.draw(g2d, componentState, outlineMode, project, drawingObserver, null); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + int diameter = getClosestOdd(width * 3 / 4); // Increased from width/2 to 3/4 of width + int borderWidth = Math.max(1, (int) STROKE_WIDTH.convertToPixels()); // Use constant stroke width + int crosshairLength = diameter * 2 / 5; // 40% of diameter (increased from 1/3) + int crosshairWidth = Math.max(1, (int) CROSSHAIR_WIDTH.convertToPixels()); // Use constant crosshair width + + // Draw white circle + g2d.setColor(Color.white); + g2d.fillOval((width - diameter) / 2, (height - diameter) / 2, diameter, diameter); + + // Draw colored border + g2d.setColor(COLOR); + g2d.setStroke(new BasicStroke(borderWidth)); + g2d.drawOval((width - diameter) / 2, (height - diameter) / 2, diameter, diameter); + + // Draw crosshairs + g2d.setStroke(new BasicStroke(crosshairWidth)); + + // Horizontal line + g2d.drawLine( + (width - crosshairLength) / 2, height / 2, + (width + crosshairLength) / 2, height / 2); + + // Vertical line + g2d.drawLine( + width / 2, (height - crosshairLength) / 2, + width / 2, (height + crosshairLength) / 2); + } + + @EditableProperty(validatorClass = PositiveNonZeroMeasureValidator.class) + public Size getDiameter() { + return diameter; + } + + public void setDiameter(Size diameter) { + this.diameter = diameter; + } + + @EditableProperty(name = "Color") + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + @Override + public String getName() { + return super.getName(); + } + + @Override + public int getControlPointCount() { + return 1; + } + + @Override + public boolean isControlPointSticky(int index) { + return true; + } + + @Override + public VisibilityPolicy getControlPointVisibilityPolicy(int index) { + return VisibilityPolicy.NEVER; + } + + @Override + public Point2D getControlPoint(int index) { + return point; + } + + @Override + public void setControlPoint(Point2D point, int index) { + this.point.setLocation(point); + } + + @Override + public Void getValue() { + return null; + } + + @Override + public String getControlPointNodeName(int index) { + return null; + } + + @Override + public void setValue(Void value) {} + + @Override + public Rectangle2D getCachingBounds() { + double diameter = getDiameter().convertToPixels(); + return new Rectangle2D.Double(point.getX() - diameter, point.getY() - diameter, + diameter * 2, diameter * 2); + } +}