def trim(vector_data: VectorData, margin_x: float, margin_y: float, layer: Union[int, List[int]]) -> VectorData: """Trim the geometries by some margin. This command trims the geometries by the provided X and Y margins with respect to the current bounding box. By default, `trim` acts on all layers. If one or more layer IDs are provided with the `--layer` option, only these layers will be affected. In this case, the bounding box is that of the listed layers. """ layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if not bounds: return vector_data min_x = bounds[0] + margin_x max_x = bounds[2] - margin_x min_y = bounds[1] + margin_y max_y = bounds[3] - margin_y if min_x > max_x: min_x = max_x = 0.5 * (min_x + max_x) if min_y > max_y: min_y = max_y = 0.5 * (min_y + max_y) for vid in layer_ids: lc = vector_data[vid] lc.crop(min_x, min_y, max_x, max_y) return vector_data
def test_vector_data_bounds_empty_layer(): vd = VectorData() vd.add(LineCollection([(0, 10 + 10j)]), 1) vd.add(LineCollection()) assert vd.bounds() == (0, 0, 10, 10)
def scale( vector_data: VectorData, scale: Tuple[float, float], layer: Union[int, List[int]], absolute: bool, keep_proportions: bool, origin_coords: Tuple[float, float], ): """Scale the geometries. The origin used is the bounding box center, unless the `--origin` option is used. By default, the arguments are used as relative factors (e.g. `scale 2 2` make the geometries twice as big in both dimensions). With `--to`, the arguments are interpreted as the final size. In this case, arguments understand the supported units (e.g. `scale --to 10cm 10cm`). By default, act on all layers. If one or more layer IDs are provided with the `--layer` option, only these layers will be affected. In this case, the bounding box is that of the listed layers. """ if vector_data.is_empty(): return vector_data # these are the layers we want to act on layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if absolute: factors = (scale[0] / (bounds[2] - bounds[0]), scale[1] / (bounds[3] - bounds[1])) if keep_proportions: factors = (min(factors), min(factors)) else: factors = scale if len(origin_coords) == 2: origin = origin_coords else: origin = ( 0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]), ) logging.info(f"scaling factors: {factors}, origin: {origin}") for vid in layer_ids: lc = vector_data[vid] lc.translate(-origin[0], -origin[1]) lc.scale(factors[0], factors[1]) lc.translate(origin[0], origin[1]) return vector_data
def rotate( vector_data: VectorData, angle: float, layer: Union[int, List[int]], radian: bool, origin_coords: Tuple[float, float], ): """ Rotate the geometries (clockwise positive). The origin used is the bounding box center, unless the `--origin` option is used. By default, act on all layers. If one or more layer IDs are provided with the `--layer` option, only these layers will be affected. In this case, the bounding box is that of the listed layers. """ if vector_data.is_empty(): return vector_data if not radian: angle *= math.pi / 180.0 # these are the layers we want to act on layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if len(origin_coords) == 2: origin = origin_coords else: origin = ( 0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]), ) logging.info(f"rotating origin: {origin}") for vid in layer_ids: lc = vector_data[vid] lc.translate(-origin[0], -origin[1]) lc.rotate(angle) lc.translate(origin[0], origin[1]) return vector_data
def skew( vector_data: VectorData, layer: Union[int, List[int]], angles: Tuple[float, float], radian: bool, origin_coords: Tuple[float, float], ): """ Skew the geometries. The geometries are sheared by the provided angles along X and Y dimensions. The origin used in the bounding box center, unless the `--centroid` or `--origin` options are used. """ if vector_data.is_empty(): return vector_data # these are the layers we want to act on layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if len(origin_coords) == 2: origin = origin_coords else: origin = ( 0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]), ) if not radian: angles = tuple(a * math.pi / 180.0 for a in angles) logging.info(f"skewing origin: {origin}") for vid in layer_ids: lc = vector_data[vid] lc.translate(-origin[0], -origin[1]) lc.skew(angles[0], angles[1]) lc.translate(origin[0], origin[1]) return vector_data
def _compute_origin( vector_data: VectorData, layer: Optional[Union[int, List[int]]], origin_coords: Union[Tuple[()], Tuple[float, float]], ) -> Tuple[Tuple[float, float], List[int], Tuple[float, float, float, float]]: layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if not bounds: logging.warning("no geometry available, cannot compute origin") raise ValueError if len(origin_coords) == 2: origin = origin_coords else: origin = ( 0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]), ) return cast(Tuple[float, float], origin), layer_ids, bounds
def dbsample(vector_data: VectorData): """ Show statistics on the current geometries in JSON format. """ global debug_data data: Dict[str, Any] = {} if vector_data.is_empty(): data["count"] = 0 else: data["count"] = sum(len(lc) for lc in vector_data.layers.values()) data["layer_count"] = len(vector_data.layers) data["length"] = vector_data.length() data["pen_up_length"] = vector_data.pen_up_length() data["bounds"] = vector_data.bounds() data["layers"] = { layer_id: [as_vector(line).tolist() for line in layer] for layer_id, layer in vector_data.layers.items() } debug_data.append(data) return vector_data
def _all_vector_data_ops(vd: VectorData): vd.bounds() vd.length() vd.segment_count()
def test_vector_data_bounds(): vd = VectorData() vd.add(LineCollection([(-10, 10), (0, 0)]), 1) vd.add(LineCollection([(0, 0), (-10j, 10j)]), 2) assert vd.bounds() == (-10, -10, 10, 10)
def write( vector_data: VectorData, output, single_path: bool, page_format: str, landscape: bool, center: bool, ): """ Save geometries to a SVG file. By default, the SVG generated has bounds tightly fit around the geometries. Optionally, a page format can be provided (`--page-format`). In this case, the geometries are not scaled or translated by default, even if they lie outside of the page bounds. The `--center` option translates the geometries to the center of the page. If output path is `-`, SVG content is output on stdout. """ if vector_data.is_empty(): logging.warning("no geometry to save, no file created") return vector_data # compute bounds bounds = vector_data.bounds() if page_format != "tight": size = tuple(c * 96.0 / 25.4 for c in PAGE_FORMATS[page_format]) if landscape: size = tuple(reversed(size)) else: size = (bounds[2] - bounds[0], bounds[3] - bounds[1]) if center: corrected_vector_data = copy.deepcopy(vector_data) corrected_vector_data.translate( (size[0] - (bounds[2] - bounds[0])) / 2.0 - bounds[0], (size[1] - (bounds[3] - bounds[1])) / 2.0 - bounds[1], ) elif page_format == "tight": corrected_vector_data = copy.deepcopy(vector_data) corrected_vector_data.translate(-bounds[0], -bounds[1]) else: corrected_vector_data = vector_data # output SVG dwg = svgwrite.Drawing(size=size, profile="tiny", debug=False) dwg.attribs["xmlns:inkscape"] = "http://www.inkscape.org/namespaces/inkscape" for layer_id in sorted(corrected_vector_data.layers.keys()): layer = corrected_vector_data.layers[layer_id] group = dwg.g( style="display:inline", id=f"layer{layer_id}", fill="none", stroke="black" ) group.attribs["inkscape:groupmode"] = "layer" group.attribs["inkscape:label"] = str(layer_id) if single_path: group.add( dwg.path( " ".join( ("M" + " L".join(f"{x},{y}" for x, y in as_vector(line))) for line in layer ), ) ) else: for line in layer: group.add(dwg.path("M" + " L".join(f"{x},{y}" for x, y in as_vector(line)),)) dwg.add(group) dwg.write(output, pretty=True) return vector_data