mirror of
https://github.com/ervanalb/keygen.git
synced 2025-12-16 05:15:28 +00:00
add inkscape extension to git and improve it to generate complex polygons, re-generate SC4
This commit is contained in:
39
keygen.scad
39
keygen.scad
@@ -1,21 +1,9 @@
|
||||
$inf=1000;
|
||||
$eps=.01;
|
||||
|
||||
/*
|
||||
BROKEN
|
||||
function key_make_complex_polygon(paths, reversed) = [
|
||||
[for (a=paths, b=a) b],
|
||||
[for(i=[0:len(reversed)])
|
||||
reversed[i]
|
||||
? [for(e=[len(paths[i])-1:-1:0]) e]
|
||||
: [for(e=[0:len(paths[i])-1]) e]
|
||||
]
|
||||
];
|
||||
*/
|
||||
|
||||
function key_move_and_scale(points, scale, offset) = [
|
||||
for(p=points) [scale[0] * (p[0] + offset[0]),
|
||||
scale[1] * (p[1] + offset[1])]
|
||||
function key_move(points, offset) = [
|
||||
for(p=points) [p[0] + offset[0],
|
||||
p[1] + offset[1]]
|
||||
];
|
||||
|
||||
module key_outline(outline_points, thickness, outline_paths=undef) {
|
||||
@@ -70,7 +58,7 @@ module key_warding_cutter(warding, blade_height, cutter_radius, left) {
|
||||
}
|
||||
|
||||
module key_emboss(emboss_points, emboss_depth, left, thickness, emboss_paths=undef) {
|
||||
translate([(left ? 1 : -1) * 0.5*thickness, 0, 0]) rotate(-90, [0, 1, 0]) rotate(-90, [0, 0, 1]) // Translate and rotate into the correct soot
|
||||
translate([(left ? -1 : 1) * 0.5*thickness, 0, 0]) rotate(-90, [0, 1, 0]) rotate(-90, [0, 0, 1]) // Translate and rotate into the correct soot
|
||||
linear_extrude(height=2*emboss_depth, center=true) // Extrude the key outline
|
||||
polygon(points=emboss_points, paths=emboss_paths); // Draw the outline
|
||||
}
|
||||
@@ -85,9 +73,8 @@ module key_blank(outline_points,
|
||||
bow_thickness=0,
|
||||
emboss_depth=.1,
|
||||
plug_diameter=0,
|
||||
scale=[1/96, -1/96],
|
||||
offset=[0, 0],
|
||||
cutter_radius=.25) {
|
||||
cutter_radius=18) {
|
||||
|
||||
// Find the bounding box of the warding
|
||||
warding_min = [min([for(e=warding) e[0]]), min([for(e=warding) e[1]])];
|
||||
@@ -95,20 +82,20 @@ module key_blank(outline_points,
|
||||
|
||||
// Apply the given offset to the outline,
|
||||
// holes, and emboss
|
||||
outline_adj = key_move_and_scale(outline_points, scale, offset);
|
||||
emboss_left_adj = key_move_and_scale(emboss_left_points, scale, offset);
|
||||
emboss_right_adj = key_move_and_scale(emboss_right_points, scale, offset);
|
||||
outline_adj = key_move(outline_points, offset);
|
||||
emboss_left_adj = key_move(emboss_left_points, offset);
|
||||
emboss_right_adj = key_move(emboss_right_points, offset);
|
||||
|
||||
// Move the warding profile
|
||||
// so that it is centered in X
|
||||
// and non-negative in Y
|
||||
warding_offset = [-0.5 * (warding_min[0] + warding_max[0]),
|
||||
-warding_max[1]];
|
||||
warding_adj = key_move_and_scale(warding, scale, warding_offset);
|
||||
-warding_min[1]];
|
||||
warding_adj = key_move(warding, warding_offset);
|
||||
|
||||
// Infer various key properties
|
||||
thickness = (bow_thickness == 0) ? abs(scale[0] * (warding_max[0] - warding_min[0])) : bow_thickness;
|
||||
blade_height = abs(scale[1] * (warding_max[1] - warding_min[1]));
|
||||
thickness = (bow_thickness == 0) ? abs(warding_max[0] - warding_min[0]) : bow_thickness;
|
||||
blade_height = abs(warding_max[1] - warding_min[1]);
|
||||
|
||||
// Cut out the warding milling artifacts
|
||||
// from the bow
|
||||
@@ -129,7 +116,7 @@ module key_blank(outline_points,
|
||||
translate([0, $inf/2, 0])
|
||||
cube([$inf, $inf, $inf], center=true);
|
||||
|
||||
key_blade(warding_adj, plug_diameter, $inf);
|
||||
key_blade(warding_adj, plug_diameter);
|
||||
}
|
||||
}
|
||||
// Draw the milling wheels that cut the warding
|
||||
|
||||
63
paths2openscad.inx
Executable file
63
paths2openscad.inx
Executable file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<_name>Paths to OpenSCAD</_name>
|
||||
<id>command.extrude.openscad</id>
|
||||
<dependency type="extension">org.inkscape.output.svg.inkscape</dependency>
|
||||
<dependency type="executable" location="extensions">paths2openscad.py</dependency>
|
||||
<dependency type="executable" location="extensions">inkex.py</dependency>
|
||||
<dependency type="executable" location="extensions">simpletransform.py</dependency>
|
||||
<dependency type="executable" location="extensions">cubicsuperpath.py</dependency>
|
||||
<dependency type="executable" location="extensions">cspsubdiv.py</dependency>
|
||||
<dependency type="executable" location="extensions">bezmisc.py</dependency>
|
||||
|
||||
<param name="tab" type="notebook">
|
||||
<page name="splash" _gui-text="Paths to OpenSCAD">
|
||||
<_param name="header" type="description" xml:space="preserve">
|
||||
|
||||
|
||||
The smoothing parameter describes
|
||||
how smoothly to render curves. Use
|
||||
smaller values for smoother curves.
|
||||
|
||||
|
||||
|
||||
|
||||
</_param>
|
||||
<param name="fname" type="string" _gui-text="Output file">~/inkscape.scad</param>
|
||||
<param name="smoothness" type="float" min="0.0001" max="5" _gui-text="Smoothing" precision="2">0.02</param>
|
||||
</page>
|
||||
<page name="info" _gui-text="About...">
|
||||
<_param name="aboutpage" type="description" xml:space="preserve">
|
||||
This extension converts Inkscape paths to
|
||||
extruded polygons in OpenSCAD. Before
|
||||
using, first convert objects to paths
|
||||
with the "Path > Object to Path"
|
||||
menu item.
|
||||
|
||||
Note that the paths must be polygonal.
|
||||
Non-polygonal paths will not render well
|
||||
in OpenSCAD. Thus, while you can convert
|
||||
text to a path, letters with closed loops
|
||||
will not appear as you expect in OpenSCAD
|
||||
(e.g., the letter "o").
|
||||
|
||||
Inkscape's units of pixels are converted
|
||||
to millimeters using the SVG Standard's
|
||||
definition of 90 pixels = 1 inch.
|
||||
|
||||
v0.9
|
||||
Dan Newman (dan newman @ mtbaldy us)
|
||||
</_param>
|
||||
</page>
|
||||
</param>
|
||||
|
||||
<effect needs-live-preview="false">
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu _name="Generate from Path"/>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command reldir="extensions" interpreter="python">paths2openscad.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
||||
886
paths2openscad.py
Executable file
886
paths2openscad.py
Executable file
@@ -0,0 +1,886 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# openscad.py
|
||||
|
||||
# This is an Inkscape extension to output paths to extruded OpenSCAD polygons
|
||||
# The Inkscape objects must first be converted to paths (Path > Object to Path).
|
||||
# Some paths may not work well -- the paths have to be polygons. As such,
|
||||
# paths derived from text may meet with mixed results.
|
||||
|
||||
# Written by Daniel C. Newman ( dan dot newman at mtbaldy dot us )
|
||||
# 10 June 2012
|
||||
|
||||
# 15 June 2012
|
||||
# Updated by Dan Newman to handle a single level of polygon nesting.
|
||||
# This is sufficient to handle most fonts.
|
||||
# If you want to nest two polygons, combine them into a single path
|
||||
# within Inkscape with "Path > Combine Path".
|
||||
|
||||
# 9 June 2017
|
||||
# Modified by Eric Van Albert to output complex polygons instead of
|
||||
# using OpenSCAD's difference()
|
||||
|
||||
# This program 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 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
import math
|
||||
import os.path
|
||||
import inkex
|
||||
import simplepath
|
||||
import simplestyle
|
||||
import simpletransform
|
||||
import cubicsuperpath
|
||||
import cspsubdiv
|
||||
import bezmisc
|
||||
import re
|
||||
|
||||
DEFAULT_WIDTH = 100
|
||||
DEFAULT_HEIGHT = 100
|
||||
|
||||
def parseLengthWithUnits( str ):
|
||||
|
||||
'''
|
||||
Parse an SVG value which may or may not have units attached
|
||||
This version is greatly simplified in that it only allows: no units,
|
||||
units of px, and units of %. Everything else, it returns None for.
|
||||
There is a more general routine to consider in scour.py if more
|
||||
generality is ever needed.
|
||||
'''
|
||||
|
||||
u = 'px'
|
||||
s = str.strip()
|
||||
if s[-2:] == 'px':
|
||||
s = s[:-2]
|
||||
elif s[-1:] == '%':
|
||||
u = '%'
|
||||
s = s[:-1]
|
||||
|
||||
try:
|
||||
v = float( s )
|
||||
except:
|
||||
return None, None
|
||||
|
||||
return v, u
|
||||
|
||||
def pointInBBox( pt, bbox ):
|
||||
|
||||
'''
|
||||
Determine if the point pt=[x, y] lies on or within the bounding
|
||||
box bbox=[xmin, xmax, ymin, ymax].
|
||||
'''
|
||||
|
||||
# if ( x < xmin ) or ( x > xmax ) or ( y < ymin ) or ( y > ymax )
|
||||
if ( pt[0] < bbox[0] ) or ( pt[0] > bbox[1] ) or \
|
||||
( pt[1] < bbox[2] ) or ( pt[1] > bbox[3] ):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def bboxInBBox( bbox1, bbox2 ):
|
||||
|
||||
'''
|
||||
Determine if the bounding box bbox1 lies on or within the
|
||||
bounding box bbox2. NOTE: we do not test for strict enclosure.
|
||||
|
||||
Structure of the bounding boxes is
|
||||
|
||||
bbox1 = [ xmin1, xmax1, ymin1, ymax1 ]
|
||||
bbox2 = [ xmin2, xmax2, ymin2, ymax2 ]
|
||||
'''
|
||||
|
||||
# if ( xmin1 < xmin2 ) or ( xmax1 > xmax2 ) or ( ymin1 < ymin2 ) or ( ymax1 > ymax2 )
|
||||
|
||||
if ( bbox1[0] < bbox2[0] ) or ( bbox1[1] > bbox2[1] ) or \
|
||||
( bbox1[2] < bbox2[2] ) or ( bbox1[3] > bbox2[3] ):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def pointInPoly( p, poly, bbox=None ):
|
||||
|
||||
'''
|
||||
Use a ray casting algorithm to see if the point p = [x, y] lies within
|
||||
the polygon poly = [[x1,y1],[x2,y2],...]. Returns True if the point
|
||||
is within poly, lies on an edge of poly, or is a vertex of poly.
|
||||
'''
|
||||
|
||||
if ( p is None ) or ( poly is None ):
|
||||
return False
|
||||
|
||||
# Check to see if the point lies outside the polygon's bounding box
|
||||
if not bbox is None:
|
||||
if not pointInBBox( p, bbox ):
|
||||
return False
|
||||
|
||||
# Check to see if the point is a vertex
|
||||
if p in poly:
|
||||
return True
|
||||
|
||||
# Handle a boundary case associated with the point
|
||||
# lying on a horizontal edge of the polygon
|
||||
x = p[0]
|
||||
y = p[1]
|
||||
p1 = poly[0]
|
||||
p2 = poly[1]
|
||||
for i in range( len( poly ) ):
|
||||
if i != 0:
|
||||
p1 = poly[i-1]
|
||||
p2 = poly[i]
|
||||
if ( y == p1[1] ) and ( p1[1] == p2[1] ) and \
|
||||
( x > min( p1[0], p2[0] ) ) and ( x < max( p1[0], p2[0] ) ):
|
||||
return True
|
||||
|
||||
n = len( poly )
|
||||
inside = False
|
||||
|
||||
p1_x,p1_y = poly[0]
|
||||
for i in range( n + 1 ):
|
||||
p2_x,p2_y = poly[i % n]
|
||||
if y > min( p1_y, p2_y ):
|
||||
if y <= max( p1_y, p2_y ):
|
||||
if x <= max( p1_x, p2_x ):
|
||||
if p1_y != p2_y:
|
||||
intersect = p1_x + (y - p1_y) * (p2_x - p1_x) / (p2_y - p1_y)
|
||||
if x <= intersect:
|
||||
inside = not inside
|
||||
else:
|
||||
inside = not inside
|
||||
p1_x,p1_y = p2_x,p2_y
|
||||
|
||||
return inside
|
||||
|
||||
def polyInPoly( poly1, bbox1, poly2, bbox2 ):
|
||||
|
||||
'''
|
||||
Determine if polygon poly2 = [[x1,y1],[x2,y2],...]
|
||||
contains polygon poly1.
|
||||
|
||||
The bounding box information, bbox=[xmin, xmax, ymin, ymax]
|
||||
is optional. When supplied it can be used to perform rejections.
|
||||
Note that one bounding box containing another is not sufficient
|
||||
to imply that one polygon contains another. It's necessary, but
|
||||
not sufficient.
|
||||
'''
|
||||
|
||||
# See if poly1's bboundin box is NOT contained by poly2's bounding box
|
||||
# if it isn't, then poly1 cannot be contained by poly2.
|
||||
|
||||
if ( not bbox1 is None ) and ( not bbox2 is None ):
|
||||
if not bboxInBBox( bbox1, bbox2 ):
|
||||
return False
|
||||
|
||||
# To see if poly1 is contained by poly2, we need to ensure that each
|
||||
# vertex of poly1 lies on or within poly2
|
||||
|
||||
for p in poly1:
|
||||
if not pointInPoly( p, poly2, bbox2 ):
|
||||
return False
|
||||
|
||||
# Looks like poly1 is contained on or in Poly2
|
||||
|
||||
return True
|
||||
|
||||
def subdivideCubicPath( sp, flat, i=1 ):
|
||||
|
||||
'''
|
||||
[ Lifted from eggbot.py with impunity ]
|
||||
|
||||
Break up a bezier curve into smaller curves, each of which
|
||||
is approximately a straight line within a given tolerance
|
||||
(the "smoothness" defined by [flat]).
|
||||
|
||||
This is a modified version of cspsubdiv.cspsubdiv(): rewritten
|
||||
because recursion-depth errors on complicated line segments
|
||||
could occur with cspsubdiv.cspsubdiv().
|
||||
'''
|
||||
|
||||
while True:
|
||||
while True:
|
||||
if i >= len( sp ):
|
||||
return
|
||||
|
||||
p0 = sp[i - 1][1]
|
||||
p1 = sp[i - 1][2]
|
||||
p2 = sp[i][0]
|
||||
p3 = sp[i][1]
|
||||
|
||||
b = ( p0, p1, p2, p3 )
|
||||
|
||||
if cspsubdiv.maxdist( b ) > flat:
|
||||
break
|
||||
|
||||
i += 1
|
||||
|
||||
one, two = bezmisc.beziersplitatt( b, 0.5 )
|
||||
sp[i - 1][2] = one[1]
|
||||
sp[i][0] = two[2]
|
||||
p = [one[2], one[3], two[1]]
|
||||
sp[i:1] = [p]
|
||||
|
||||
class OpenSCAD( inkex.Effect ):
|
||||
|
||||
def __init__( self ):
|
||||
|
||||
inkex.Effect.__init__( self )
|
||||
|
||||
self.OptionParser.add_option( "--tab", #NOTE: value is not used.
|
||||
action="store", type="string",
|
||||
dest="tab", default="splash",
|
||||
help="The active tab when Apply was pressed" )
|
||||
|
||||
self.OptionParser.add_option('--smoothness', dest='smoothness',
|
||||
type='float', default=float( 0.2 ), action='store',
|
||||
help='Curve smoothing (less for more)' )
|
||||
|
||||
self.OptionParser.add_option('--fname', dest='fname',
|
||||
type='string', default='~/inkscape.scad',
|
||||
action='store',
|
||||
help='Curve smoothing (less for more)' )
|
||||
|
||||
self.cx = float( DEFAULT_WIDTH ) / 2.0
|
||||
self.cy = float( DEFAULT_HEIGHT ) / 2.0
|
||||
self.xmin, self.xmax = ( 1.0E70, -1.0E70 )
|
||||
self.ymin, self.ymax = ( 1.0E70, -1.0E70 )
|
||||
|
||||
# Dictionary of paths we will construct. It's keyed by the SVG node
|
||||
# it came from. Such keying isn't too useful in this specific case,
|
||||
# but it can be useful in other applications when you actually want
|
||||
# to go back and update the SVG document
|
||||
self.paths = {}
|
||||
|
||||
# Output file handling
|
||||
self.call_list = []
|
||||
self.pathid = int( 0 )
|
||||
|
||||
# Output file
|
||||
self.f = None
|
||||
|
||||
# For handling an SVG viewbox attribute, we will need to know the
|
||||
# values of the document's <svg> width and height attributes as well
|
||||
# as establishing a transform from the viewbox to the display.
|
||||
|
||||
self.docWidth = float( DEFAULT_WIDTH )
|
||||
self.docHeight = float( DEFAULT_HEIGHT )
|
||||
self.docTransform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||||
|
||||
# Dictionary of warnings issued. This to prevent from warning
|
||||
# multiple times about the same problem
|
||||
self.warnings = {}
|
||||
|
||||
def getLength( self, name, default ):
|
||||
|
||||
'''
|
||||
Get the <svg> attribute with name "name" and default value "default"
|
||||
Parse the attribute into a value and associated units. Then, accept
|
||||
units of cm, ft, in, m, mm, pc, or pt. Convert to pixels.
|
||||
|
||||
Note that SVG defines 90 px = 1 in = 25.4 mm.
|
||||
'''
|
||||
|
||||
str = self.document.getroot().get( name )
|
||||
if str:
|
||||
v, u = parseLengthWithUnits( str )
|
||||
if not v:
|
||||
# Couldn't parse the value
|
||||
return None
|
||||
elif ( u == 'mm' ):
|
||||
return float( v ) * ( 90.0 / 25.4 )
|
||||
elif ( u == 'cm' ):
|
||||
return float( v ) * ( 90.0 * 10.0 / 25.4 )
|
||||
elif ( u == 'm' ):
|
||||
return float( v ) * ( 90.0 * 1000.0 / 25.4 )
|
||||
elif ( u == 'in' ):
|
||||
return float( v ) * 90.0
|
||||
elif ( u == 'ft' ):
|
||||
return float( v ) * 12.0 * 90.0
|
||||
elif ( u == 'pt' ):
|
||||
# Use modern "Postscript" points of 72 pt = 1 in instead
|
||||
# of the traditional 72.27 pt = 1 in
|
||||
return float( v ) * ( 90.0 / 72.0 )
|
||||
elif ( u == 'pc' ):
|
||||
return float( v ) * ( 90.0 / 6.0 )
|
||||
elif ( u == 'px' ):
|
||||
return float( v )
|
||||
else:
|
||||
# Unsupported units
|
||||
return None
|
||||
else:
|
||||
# No width specified; assume the default value
|
||||
return float( default )
|
||||
|
||||
def getDocProps( self ):
|
||||
|
||||
'''
|
||||
Get the document's height and width attributes from the <svg> tag.
|
||||
Use a default value in case the property is not present or is
|
||||
expressed in units of percentages.
|
||||
'''
|
||||
|
||||
self.docHeight = self.getLength( 'height', DEFAULT_HEIGHT )
|
||||
self.docWidth = self.getLength( 'width', DEFAULT_WIDTH )
|
||||
if ( self.docHeight == None ) or ( self.docWidth == None ):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def handleViewBox( self ):
|
||||
|
||||
'''
|
||||
Set up the document-wide transform in the event that the document has an SVG viewbox
|
||||
'''
|
||||
|
||||
if self.getDocProps():
|
||||
viewbox = self.document.getroot().get( 'viewBox' )
|
||||
if viewbox:
|
||||
vinfo = viewbox.strip().replace( ',', ' ' ).split( ' ' )
|
||||
if ( vinfo[2] != 0 ) and ( vinfo[3] != 0 ):
|
||||
sx = self.docWidth / float( vinfo[2] )
|
||||
sy = self.docHeight / float( vinfo[3] )
|
||||
self.docTransform = simpletransform.parseTransform( 'scale(%f,%f)' % (sx, sy) )
|
||||
|
||||
def getPathVertices( self, path, node=None, transform=None ):
|
||||
|
||||
'''
|
||||
Decompose the path data from an SVG element into individual
|
||||
subpaths, each subpath consisting of absolute move to and line
|
||||
to coordinates. Place these coordinates into a list of polygon
|
||||
vertices.
|
||||
'''
|
||||
|
||||
if ( not path ) or ( len( path ) == 0 ):
|
||||
# Nothing to do
|
||||
return None
|
||||
|
||||
# parsePath() may raise an exception. This is okay
|
||||
sp = simplepath.parsePath( path )
|
||||
if ( not sp ) or ( len( sp ) == 0 ):
|
||||
# Path must have been devoid of any real content
|
||||
return None
|
||||
|
||||
# Get a cubic super path
|
||||
p = cubicsuperpath.CubicSuperPath( sp )
|
||||
if ( not p ) or ( len( p ) == 0 ):
|
||||
# Probably never happens, but...
|
||||
return None
|
||||
|
||||
if transform:
|
||||
simpletransform.applyTransformToPath( transform, p )
|
||||
|
||||
# Now traverse the cubic super path
|
||||
subpath_list = []
|
||||
subpath_vertices = []
|
||||
|
||||
for sp in p:
|
||||
|
||||
# We've started a new subpath
|
||||
# See if there is a prior subpath and whether we should keep it
|
||||
if len( subpath_vertices ):
|
||||
subpath_list.append( [ subpath_vertices, [ sp_xmin, sp_xmax, sp_ymin, sp_ymax ] ] )
|
||||
|
||||
subpath_vertices = []
|
||||
subdivideCubicPath( sp, float( self.options.smoothness ) )
|
||||
|
||||
# Note the first point of the subpath
|
||||
first_point = sp[0][1]
|
||||
subpath_vertices.append( first_point )
|
||||
sp_xmin = first_point[0]
|
||||
sp_xmax = first_point[0]
|
||||
sp_ymin = first_point[1]
|
||||
sp_ymax = first_point[1]
|
||||
|
||||
# See if the first and last points are identical
|
||||
# OpenSCAD doesn't mind if we duplicate the first and last
|
||||
# vertex, but our polygon in polygon algorithm may
|
||||
n = len( sp )
|
||||
last_point = sp[n-1][1]
|
||||
if ( first_point[0] == last_point[0] ) and ( first_point[1] == last_point[1] ):
|
||||
n = n - 1
|
||||
|
||||
# Traverse each point of the subpath
|
||||
for csp in sp[1:n]:
|
||||
|
||||
# Append the vertex to our list of vertices
|
||||
pt = csp[1]
|
||||
subpath_vertices.append( pt )
|
||||
|
||||
# Track the bounding box of this subpath
|
||||
if pt[0] < sp_xmin:
|
||||
sp_xmin = pt[0]
|
||||
elif pt[0] > sp_xmax:
|
||||
sp_xmax = pt[0]
|
||||
if pt[1] < sp_ymin:
|
||||
sp_ymin = pt[1]
|
||||
elif pt[1] > sp_ymax:
|
||||
sp_ymax = pt[1]
|
||||
|
||||
# Track the bounding box of the overall drawing
|
||||
# This is used for centering the polygons in OpenSCAD around the (x,y) origin
|
||||
if sp_xmin < self.xmin:
|
||||
self.xmin = sp_xmin
|
||||
if sp_xmax > self.xmax:
|
||||
self.xmax = sp_xmax
|
||||
if sp_ymin < self.ymin:
|
||||
self.ymin = sp_ymin
|
||||
if sp_ymax > self.ymax:
|
||||
self.ymax = sp_ymax
|
||||
|
||||
# Handle the final subpath
|
||||
if len( subpath_vertices ):
|
||||
subpath_list.append( [ subpath_vertices, [ sp_xmin, sp_xmax, sp_ymin, sp_ymax ] ] )
|
||||
|
||||
if len( subpath_list ) > 0:
|
||||
self.paths[node] = subpath_list
|
||||
|
||||
def convertPath( self, node ):
|
||||
|
||||
path = self.paths[node]
|
||||
if ( path is None ) or ( len( path ) == 0 ):
|
||||
return
|
||||
|
||||
# Determine which polys contain which
|
||||
|
||||
contains = [ [] for i in xrange( len( path ) ) ]
|
||||
contained_by = [ [] for i in xrange( len( path ) ) ]
|
||||
|
||||
for i in range( 0, len( path ) ):
|
||||
for j in range( i + 1, len( path ) ):
|
||||
if polyInPoly( path[j][0], path[j][1], path[i][0], path[i][1] ):
|
||||
# subpath i contains subpath j
|
||||
contains[i].append( j )
|
||||
# subpath j is contained in subpath i
|
||||
contained_by[j].append( i )
|
||||
elif polyInPoly( path[i][0], path[i][1], path[j][0], path[j][1] ):
|
||||
# subpath j contains subpath i
|
||||
contains[j].append( i )
|
||||
# subpath i is containd in subpath j
|
||||
contained_by[i].append( j )
|
||||
|
||||
# Generate an OpenSCAD module for this path
|
||||
id = node.get ( 'id', '' )
|
||||
if ( id is None ) or ( id == '' ):
|
||||
id = str( self.pathid ) + 'x'
|
||||
self.pathid += 1
|
||||
else:
|
||||
id = re.sub( '[^A-Za-z0-9_]+', '', id )
|
||||
scale = (1, -1) # We appear to be working in mm by this point
|
||||
|
||||
# And add the call to the call list
|
||||
self.call_list.append( '//polygon(points={0}_points, paths={0}_paths);\n'.format(id) )
|
||||
|
||||
points = []
|
||||
paths = []
|
||||
|
||||
for i in range( 0, len( path ) ):
|
||||
|
||||
# Skip this subpath if it is contained by another one
|
||||
if len( contained_by[i] ) != 0:
|
||||
continue
|
||||
|
||||
subpath = path[i][0]
|
||||
bbox = path[i][1]
|
||||
|
||||
one_path = []
|
||||
for point in subpath:
|
||||
one_path.append(len(points))
|
||||
points.append((point[0] - self.cx, point[1] - self.cy ))
|
||||
|
||||
paths.append(one_path)
|
||||
|
||||
if len( contains[i] ) != 0:
|
||||
for j in contains[i]:
|
||||
one_path = []
|
||||
for point in path[j][0]:
|
||||
one_path.append(len(points))
|
||||
points.append((point[0] - self.cx, point[1] - self.cy ))
|
||||
|
||||
paths.append(list(reversed(one_path)))
|
||||
|
||||
points_str = "[{}]".format(", ".join(["[{:8f}, {:8f}]".format(x * scale[0], y * scale[1]) for (x, y) in points]))
|
||||
paths_str = "[{}]".format(", ".join(["[{}]".format(", ".join(["{:d}".format(i) for i in indices])) for indices in paths]))
|
||||
|
||||
self.f.write( '{}_points = {};\n'.format(id, points_str) )
|
||||
self.f.write( '{}_paths = {};\n'.format(id, paths_str) )
|
||||
|
||||
def recursivelyTraverseSvg( self, aNodeList,
|
||||
matCurrent=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
|
||||
parent_visibility='visible' ):
|
||||
|
||||
'''
|
||||
[ This too is largely lifted from eggbot.py ]
|
||||
|
||||
Recursively walk the SVG document, building polygon vertex lists
|
||||
for each graphical element we support.
|
||||
|
||||
Rendered SVG elements:
|
||||
<circle>, <ellipse>, <line>, <path>, <polygon>, <polyline>, <rect>
|
||||
|
||||
Supported SVG elements:
|
||||
<group>, <use>
|
||||
|
||||
Ignored SVG elements:
|
||||
<defs>, <eggbot>, <metadata>, <namedview>, <pattern>,
|
||||
processing directives
|
||||
|
||||
All other SVG elements trigger an error (including <text>)
|
||||
'''
|
||||
|
||||
for node in aNodeList:
|
||||
|
||||
# Ignore invisible nodes
|
||||
v = node.get( 'visibility', parent_visibility )
|
||||
if v == 'inherit':
|
||||
v = parent_visibility
|
||||
if v == 'hidden' or v == 'collapse':
|
||||
pass
|
||||
|
||||
# First apply the current matrix transform to this node's tranform
|
||||
matNew = simpletransform.composeTransform( matCurrent, simpletransform.parseTransform( node.get( "transform" ) ) )
|
||||
|
||||
if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g':
|
||||
|
||||
self.recursivelyTraverseSvg( node, matNew, v )
|
||||
|
||||
elif node.tag == inkex.addNS( 'use', 'svg' ) or node.tag == 'use':
|
||||
|
||||
# A <use> element refers to another SVG element via an xlink:href="#blah"
|
||||
# attribute. We will handle the element by doing an XPath search through
|
||||
# the document, looking for the element with the matching id="blah"
|
||||
# attribute. We then recursively process that element after applying
|
||||
# any necessary (x,y) translation.
|
||||
#
|
||||
# Notes:
|
||||
# 1. We ignore the height and width attributes as they do not apply to
|
||||
# path-like elements, and
|
||||
# 2. Even if the use element has visibility="hidden", SVG still calls
|
||||
# for processing the referenced element. The referenced element is
|
||||
# hidden only if its visibility is "inherit" or "hidden".
|
||||
|
||||
refid = node.get( inkex.addNS( 'href', 'xlink' ) )
|
||||
if not refid:
|
||||
pass
|
||||
|
||||
# [1:] to ignore leading '#' in reference
|
||||
path = '//*[@id="%s"]' % refid[1:]
|
||||
refnode = node.xpath( path )
|
||||
if refnode:
|
||||
x = float( node.get( 'x', '0' ) )
|
||||
y = float( node.get( 'y', '0' ) )
|
||||
# Note: the transform has already been applied
|
||||
if ( x != 0 ) or (y != 0 ):
|
||||
matNew2 = composeTransform( matNew, parseTransform( 'translate(%f,%f)' % (x,y) ) )
|
||||
else:
|
||||
matNew2 = matNew
|
||||
v = node.get( 'visibility', v )
|
||||
self.recursivelyTraverseSvg( refnode, matNew2, v )
|
||||
|
||||
elif node.tag == inkex.addNS( 'path', 'svg' ):
|
||||
|
||||
path_data = node.get( 'd')
|
||||
if path_data:
|
||||
self.getPathVertices( path_data, node, matNew )
|
||||
|
||||
elif node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect':
|
||||
|
||||
# Manually transform
|
||||
#
|
||||
# <rect x="X" y="Y" width="W" height="H"/>
|
||||
#
|
||||
# into
|
||||
#
|
||||
# <path d="MX,Y lW,0 l0,H l-W,0 z"/>
|
||||
#
|
||||
# I.e., explicitly draw three sides of the rectangle and the
|
||||
# fourth side implicitly
|
||||
|
||||
# Create a path with the outline of the rectangle
|
||||
x = float( node.get( 'x' ) )
|
||||
y = float( node.get( 'y' ) )
|
||||
if ( not x ) or ( not y ):
|
||||
pass
|
||||
w = float( node.get( 'width', '0' ) )
|
||||
h = float( node.get( 'height', '0' ) )
|
||||
a = []
|
||||
a.append( ['M ', [x, y]] )
|
||||
a.append( [' l ', [w, 0]] )
|
||||
a.append( [' l ', [0, h]] )
|
||||
a.append( [' l ', [-w, 0]] )
|
||||
a.append( [' Z', []] )
|
||||
self.getPathVertices( simplepath.formatPath( a ), node, matNew )
|
||||
|
||||
elif node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line':
|
||||
|
||||
# Convert
|
||||
#
|
||||
# <line x1="X1" y1="Y1" x2="X2" y2="Y2/>
|
||||
#
|
||||
# to
|
||||
#
|
||||
# <path d="MX1,Y1 LX2,Y2"/>
|
||||
|
||||
x1 = float( node.get( 'x1' ) )
|
||||
y1 = float( node.get( 'y1' ) )
|
||||
x2 = float( node.get( 'x2' ) )
|
||||
y2 = float( node.get( 'y2' ) )
|
||||
if ( not x1 ) or ( not y1 ) or ( not x2 ) or ( not y2 ):
|
||||
pass
|
||||
a = []
|
||||
a.append( ['M ', [x1, y1]] )
|
||||
a.append( [' L ', [x2, y2]] )
|
||||
self.getPathVertices( simplepath.formatPath( a ), node, matNew )
|
||||
|
||||
elif node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline':
|
||||
|
||||
# Convert
|
||||
#
|
||||
# <polyline points="x1,y1 x2,y2 x3,y3 [...]"/>
|
||||
#
|
||||
# to
|
||||
#
|
||||
# <path d="Mx1,y1 Lx2,y2 Lx3,y3 [...]"/>
|
||||
#
|
||||
# Note: we ignore polylines with no points
|
||||
|
||||
pl = node.get( 'points', '' ).strip()
|
||||
if pl == '':
|
||||
pass
|
||||
|
||||
pa = pl.split()
|
||||
d = "".join( ["M " + pa[i] if i == 0 else " L " + pa[i] for i in range( 0, len( pa ) )] )
|
||||
self.getPathVertices( d, node, matNew )
|
||||
|
||||
elif node.tag == inkex.addNS( 'polygon', 'svg' ) or node.tag == 'polygon':
|
||||
|
||||
# Convert
|
||||
#
|
||||
# <polygon points="x1,y1 x2,y2 x3,y3 [...]"/>
|
||||
#
|
||||
# to
|
||||
#
|
||||
# <path d="Mx1,y1 Lx2,y2 Lx3,y3 [...] Z"/>
|
||||
#
|
||||
# Note: we ignore polygons with no points
|
||||
|
||||
pl = node.get( 'points', '' ).strip()
|
||||
if pl == '':
|
||||
pass
|
||||
|
||||
pa = pl.split()
|
||||
d = "".join( ["M " + pa[i] if i == 0 else " L " + pa[i] for i in range( 0, len( pa ) )] )
|
||||
d += " Z"
|
||||
self.getPathVertices( d, node, matNew )
|
||||
|
||||
elif node.tag == inkex.addNS( 'ellipse', 'svg' ) or \
|
||||
node.tag == 'ellipse' or \
|
||||
node.tag == inkex.addNS( 'circle', 'svg' ) or \
|
||||
node.tag == 'circle':
|
||||
|
||||
# Convert circles and ellipses to a path with two 180 degree arcs.
|
||||
# In general (an ellipse), we convert
|
||||
#
|
||||
# <ellipse rx="RX" ry="RY" cx="X" cy="Y"/>
|
||||
#
|
||||
# to
|
||||
#
|
||||
# <path d="MX1,CY A RX,RY 0 1 0 X2,CY A RX,RY 0 1 0 X1,CY"/>
|
||||
#
|
||||
# where
|
||||
#
|
||||
# X1 = CX - RX
|
||||
# X2 = CX + RX
|
||||
#
|
||||
# Note: ellipses or circles with a radius attribute of value 0 are ignored
|
||||
|
||||
if node.tag == inkex.addNS( 'ellipse', 'svg' ) or node.tag == 'ellipse':
|
||||
rx = float( node.get( 'rx', '0' ) )
|
||||
ry = float( node.get( 'ry', '0' ) )
|
||||
else:
|
||||
rx = float( node.get( 'r', '0' ) )
|
||||
ry = rx
|
||||
if rx == 0 or ry == 0:
|
||||
pass
|
||||
|
||||
cx = float( node.get( 'cx', '0' ) )
|
||||
cy = float( node.get( 'cy', '0' ) )
|
||||
x1 = cx - rx
|
||||
x2 = cx + rx
|
||||
d = 'M %f,%f ' % ( x1, cy ) + \
|
||||
'A %f,%f ' % ( rx, ry ) + \
|
||||
'0 1 0 %f,%f ' % ( x2, cy ) + \
|
||||
'A %f,%f ' % ( rx, ry ) + \
|
||||
'0 1 0 %f,%f' % ( x1, cy )
|
||||
self.mapPathVertices( d, node, matNew )
|
||||
|
||||
elif node.tag == inkex.addNS( 'pattern', 'svg' ) or node.tag == 'pattern':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'metadata', 'svg' ) or node.tag == 'metadata':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'defs', 'svg' ) or node.tag == 'defs':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'desc', 'svg' ) or node.tag == 'desc':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'namedview', 'sodipodi' ) or node.tag == 'namedview':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'eggbot', 'svg' ) or node.tag == 'eggbot':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'text', 'svg' ) or node.tag == 'text':
|
||||
|
||||
inkex.errormsg( 'Warning: unable to draw text, please convert it to a path first.' )
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'title', 'svg' ) or node.tag == 'title':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'image', 'svg' ) or node.tag == 'image':
|
||||
|
||||
if not self.warnings.has_key( 'image' ):
|
||||
inkex.errormsg( 'Warning: unable to draw bitmap images; ' +
|
||||
'please convert them to line art first. Consider using the "Trace bitmap..." ' +
|
||||
'tool of the "Path" menu. Mac users please note that some X11 settings may ' +
|
||||
'cause cut-and-paste operations to paste in bitmap copies.' )
|
||||
self.warnings['image'] = 1
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'pattern', 'svg' ) or node.tag == 'pattern':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'radialGradient', 'svg' ) or node.tag == 'radialGradient':
|
||||
|
||||
# Similar to pattern
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'linearGradient', 'svg' ) or node.tag == 'linearGradient':
|
||||
|
||||
# Similar in pattern
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'style', 'svg' ) or node.tag == 'style':
|
||||
|
||||
# This is a reference to an external style sheet and not the value
|
||||
# of a style attribute to be inherited by child elements
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'cursor', 'svg' ) or node.tag == 'cursor':
|
||||
|
||||
pass
|
||||
|
||||
elif node.tag == inkex.addNS( 'color-profile', 'svg' ) or node.tag == 'color-profile':
|
||||
|
||||
# Gamma curves, color temp, etc. are not relevant to single color output
|
||||
pass
|
||||
|
||||
elif not isinstance( node.tag, basestring ):
|
||||
|
||||
# This is likely an XML processing instruction such as an XML
|
||||
# comment. lxml uses a function reference for such node tags
|
||||
# and as such the node tag is likely not a printable string.
|
||||
# Further, converting it to a printable string likely won't
|
||||
# be very useful.
|
||||
|
||||
pass
|
||||
|
||||
else:
|
||||
|
||||
inkex.errormsg( 'Warning: unable to draw object <%s>, please convert it to a path first.' % node.tag )
|
||||
pass
|
||||
|
||||
|
||||
def recursivelyGetEnclosingTransform( self, node ):
|
||||
|
||||
'''
|
||||
Determine the cumulative transform which node inherits from
|
||||
its chain of ancestors.
|
||||
'''
|
||||
node = node.getparent()
|
||||
if node is not None:
|
||||
parent_transform = self.recursivelyGetEnclosingTransform( node )
|
||||
node_transform = node.get( 'transform', None )
|
||||
if node_transform is None:
|
||||
return parent_transform
|
||||
else:
|
||||
tr = simpletransform.parseTransform( node_transform )
|
||||
if parent_transform is None:
|
||||
return tr
|
||||
else:
|
||||
return simpletransform.composeTransform( parent_transform, tr )
|
||||
else:
|
||||
return self.docTransform
|
||||
|
||||
def effect( self ):
|
||||
|
||||
# Viewbox handling
|
||||
self.handleViewBox()
|
||||
|
||||
# First traverse the document (or selected items), reducing
|
||||
# everything to line segments. If working on a selection,
|
||||
# then determine the selection's bounding box in the process.
|
||||
# (Actually, we just need to know it's extrema on the x-axis.)
|
||||
|
||||
if self.options.ids:
|
||||
# Traverse the selected objects
|
||||
for id in self.options.ids:
|
||||
transform = self.recursivelyGetEnclosingTransform( self.selected[id] )
|
||||
self.recursivelyTraverseSvg( [self.selected[id]], transform )
|
||||
else:
|
||||
# Traverse the entire document building new, transformed paths
|
||||
self.recursivelyTraverseSvg( self.document.getroot(), self.docTransform )
|
||||
|
||||
# Determine the center of the drawing's bounding box
|
||||
self.cx = self.xmin + ( self.xmax - self.xmin ) / 2.0
|
||||
self.cy = self.ymin + ( self.ymax - self.ymin ) / 2.0
|
||||
|
||||
# Determine which polygons lie entirely within other polygons
|
||||
try:
|
||||
if '/' == os.sep:
|
||||
self.f = open( os.path.expanduser( self.options.fname ), 'w')
|
||||
else:
|
||||
self.f = open( os.path.expanduser( self.options.fname ).replace('/', os.sep), 'w')
|
||||
|
||||
self.f.write('''// Automatically generated using the Inkscape to OpenSCAD Converter
|
||||
// Variable names are of the form <inkscape-path-id>_points and
|
||||
// <inkscape-path-id>_paths. As a result, you can associate a polygon in this
|
||||
// OpenSCAD program with the corresponding SVG element in the Inkscape document
|
||||
// by looking for the XML element with the attribute id=\"inkscape-path-id\".
|
||||
''' )
|
||||
|
||||
for key in self.paths:
|
||||
self.f.write( '\n' )
|
||||
self.convertPath( key )
|
||||
|
||||
# Now output the list of modules to call
|
||||
self.f.write( '\n' )
|
||||
for call in self.call_list:
|
||||
self.f.write( call )
|
||||
|
||||
except IOError:
|
||||
inkex.errormsg( 'Unable to open the file ' + self.options.fname )
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
e = OpenSCAD()
|
||||
e.affect()
|
||||
37
sc4.scad
37
sc4.scad
@@ -1,30 +1,11 @@
|
||||
use <keygen.scad>
|
||||
include <sc4_polygons.scad>
|
||||
|
||||
outline = [[-12.048077,5.767320],[-12.531197,6.197850],[-16.188977,6.288450],[-16.558561,6.416520],[-16.805557,6.775740],[-17.059163,7.256309],[-17.405057,7.424420],[-19.734005,7.436020],[-20.190575,7.678950],[-21.005688,8.407776],[-21.584899,8.714600],[-21.950541,9.552540],[-22.161920,10.454470],[-22.664611,10.714781],[-23.160009,10.840370],[-23.513154,11.702385],[-23.804236,12.163606],[-24.352135,12.364150],[-26.715326,12.364150],[-27.280510,12.190678],[-27.452528,11.780981],[-27.624588,10.917230],[-28.773427,10.437810],[-29.001442,9.531850],[-29.229456,8.625890],[-29.751429,8.441815],[-30.273401,8.257560],[-30.510304,7.256071],[-30.624192,6.261970],[-31.166270,5.957583],[-31.684671,5.815030],[-35.921242,3.350340],[-36.420288,2.840467],[-36.507637,2.275740],[-36.507626,-2.275740],[-36.420275,-2.840468],[-35.921231,-3.350340],[-31.684660,-5.815030],[-31.166259,-5.957590],[-30.624181,-6.261970],[-30.510293,-7.256071],[-30.273390,-8.257560],[-29.751418,-8.441725],[-29.229445,-8.625890],[-29.001431,-9.531850],[-28.773416,-10.437810],[-27.624577,-10.917230],[-27.452517,-11.780981],[-27.280499,-12.190678],[-26.715315,-12.364150],[-24.352124,-12.364150],[-23.804225,-12.163606],[-23.513143,-11.702385],[-23.159998,-10.840370],[-22.664600,-10.714774],[-22.161909,-10.454470],[-21.950531,-9.552540],[-21.584888,-8.714600],[-21.005677,-8.407776],[-20.190564,-7.678950],[-19.733994,-7.436020],[-17.405047,-7.424420],[-17.059153,-7.256309],[-16.805547,-6.775740],[-16.558555,-6.416520],[-16.188967,-6.288450],[-12.531187,-6.197850],[-12.048067,-5.767320],[-12.026867,-4.485730],[13.592243,-4.486130],[18.673063,0.615172],[18.673063,1.913110],[16.250473,4.230870],[-12.047637,4.226870]];
|
||||
|
||||
hole = [[-28.090168,3.691950],[-28.320547,4.292520],[-29.442927,4.261420],[-33.529647,1.963740],[-34.149378,1.203692],[-34.149367,-1.203652],[-33.529636,-1.963700],[-29.442916,-4.261380],[-28.320536,-4.292480],[-28.090157,-3.691910]];
|
||||
|
||||
warding = [[34.597676,-4.486348],[34.597676,-0.102120],[35.469973,0.280288],[35.751594,0.538961],[35.618801,0.933478],[34.597676,2.619678],[34.597676,4.230951],[36.507637,4.230951],[36.507637,3.395860],[35.935063,3.114221],[35.700383,2.824692],[35.746960,2.487905],[36.507637,1.173773],[36.507637,-0.570308],[35.618801,-0.848327],[35.618801,-4.486348],[34.597676,-4.486348]];
|
||||
|
||||
emboss_points = [[-23.687656,-12.029039],[-23.687656,12.028407],[-23.424104,11.446530],[-23.424104,-11.447162],[-23.687656,-12.029039]];
|
||||
/*
|
||||
Additional Embossing
|
||||
linear_extrude(height=h)
|
||||
polygon([[-27.515846,-11.120050],[-27.624364,-10.917479],[-27.781461,-10.800690],[-27.781461,10.800574],[-27.624364,10.916847],[-27.515846,11.119418],[-27.515846,-11.120050]]);
|
||||
linear_extrude(height=h)
|
||||
polygon([[-22.144595,-10.426037],[-22.144595,10.425405],[-21.878980,9.172251],[-21.878980,-9.172883],[-22.144595,-10.426037]]);
|
||||
linear_extrude(height=h)
|
||||
polygon([[-29.082158,-8.830269],[-29.229435,-8.626145],[-29.347773,-8.533128],[-29.347773,-4.304446],[-29.082158,-4.375760],[-29.082158,-8.830269]]);
|
||||
linear_extrude(height=h)
|
||||
polygon([[-20.716776,-8.206016],[-20.716776,8.205384],[-20.453227,7.968190],[-20.453227,-7.968822],[-20.716776,-8.206016]]);
|
||||
linear_extrude(height=h)
|
||||
polygon([[-17.000209,-7.198327],[-17.000209,7.197695],[-16.805388,6.775496],[-16.734591,6.580678],[-16.734591,-6.581310],[-16.805388,-6.776131],[-17.000209,-7.198327]]);
|
||||
linear_extrude(height=h)
|
||||
polygon([[-29.347773,4.303814],[-29.347773,8.532496],[-29.229435,8.625513],[-29.082158,8.829637],[-29.082158,4.375128],[-29.347773,4.303814]]);
|
||||
*/
|
||||
|
||||
outline_points = concat(outline, hole);
|
||||
outline_paths = [[for(i=[0:len(outline)-1]) i],
|
||||
[for(i=[len(hole)-1:-1:0]) i+len(outline)]];
|
||||
|
||||
key_blank(outline_points, warding, outline_paths=outline_paths, offset=-outline_points[len(outline)-1]);
|
||||
key_blank(outline_points,
|
||||
warding_points,
|
||||
outline_paths=outline_paths,
|
||||
emboss_right_points=emboss_points,
|
||||
emboss_right_paths=emboss_paths,
|
||||
emboss_left_points=emboss_points,
|
||||
emboss_left_paths=emboss_paths,
|
||||
offset=-outline_points[187]);
|
||||
26
sc4.svg
26
sc4.svg
@@ -50,8 +50,8 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="371.70151"
|
||||
inkscape:cy="761.25519"
|
||||
inkscape:cx="363.99281"
|
||||
inkscape:cy="776.68608"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
@@ -59,7 +59,8 @@
|
||||
inkscape:window-height="1767"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="33"
|
||||
inkscape:window-maximized="0" />
|
||||
inkscape:window-maximized="0"
|
||||
units="mm" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
@@ -68,7 +69,7 @@
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
@@ -103,16 +104,17 @@
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 89.968469,102.00306 c 0.0066,0.39155 -0.05364,0.4569 -0.230379,0.60057 -0.176738,0.14367 -0.807458,0.15071 -1.12238,-0.0311 -0.314922,-0.18182 -3.6919,-2.06973 -4.08672,-2.29768 -0.39482,-0.22795 -0.619731,-0.44483 -0.619731,-0.760048 l 1.1e-5,-2.407344 c 0,-0.315218 0.224911,-0.532098 0.619731,-0.760048 0.39482,-0.22795 3.771798,-2.11586 4.08672,-2.29768 0.314922,-0.18182 0.945642,-0.17478 1.12238,-0.0311 0.176738,0.14367 0.23696,0.20902 0.230379,0.60057 z m 16.042091,2.07537 c 0,0.22836 -0.23197,0.43053 -0.48312,0.43053 -0.25115,0 -3.48023,0.0906 -3.65778,0.0906 -0.17756,0 -0.60247,0.17909 -0.61658,0.48729 -0.0191,0.41661 -0.45735,0.64868 -0.5995,0.64868 -0.14215,0 -1.570935,0.009 -2.328948,0.0116 -0.144144,4.3e-4 -0.356676,0.12247 -0.45657,0.24293 -0.664839,0.80167 -1.044022,0.79665 -1.394324,1.03565 -0.529527,0.36129 -0.253177,1.29326 -0.577021,1.73987 -0.323844,0.44662 -0.683968,0.11891 -0.998089,0.3859 -0.314121,0.267 -0.230223,1.52378 -1.192126,1.52378 h -2.363191 c -1.127636,0 -0.535148,-1.07281 -0.909262,-1.44692 -0.347955,-0.34796 -0.800885,-0.13146 -1.148839,-0.47942 -0.52905,-0.52905 0.07302,-1.28287 -0.456029,-1.81192 -0.370084,-0.37009 -0.673861,0.002 -1.043945,-0.36833 -0.49379,-0.49379 -0.02102,-1.51165 -0.350791,-1.99559 -0.246109,-0.36116 -0.845938,-0.30156 -1.060479,-0.44694 -0.10727,-0.0727 -3.687643,-2.13726 -4.236571,-2.46469 -0.548928,-0.32743 -0.586395,-0.67403 -0.586395,-1.0746 l 1.1e-5,-4.55148 c 0,-0.40057 0.03747,-0.74717 0.586395,-1.0746 0.548928,-0.32743 4.129301,-2.39199 4.236571,-2.46469 0.214541,-0.14538 0.81437,-0.0858 1.060479,-0.44694 0.329771,-0.48394 -0.142999,-1.5018 0.350791,-1.99559 0.370084,-0.37033 0.673861,0.002 1.043945,-0.36833 0.529049,-0.52905 -0.07302,-1.28287 0.456029,-1.81192 0.347954,-0.34796 0.800884,-0.13146 1.148839,-0.47942 0.374114,-0.37411 -0.218374,-1.44692 0.909262,-1.44692 h 2.363191 c 0.961903,0 0.878005,1.25678 1.192126,1.52378 0.314121,0.26699 0.674245,-0.0607 0.998089,0.3859 0.323844,0.44661 0.04749,1.37858 0.577021,1.73987 0.350302,0.239 0.729485,0.23398 1.394324,1.03565 0.09989,0.12046 0.312426,0.2425 0.45657,0.24293 0.758013,0.003 2.186797,0.0116 2.328947,0.0116 0.14215,0 0.5804,0.23207 0.5995,0.64868 0.0141,0.3082 0.43902,0.48729 0.61658,0.48729 0.17755,0 3.40663,0.0906 3.65778,0.0906 0.25115,0 0.48312,0.20217 0.48312,0.43053 0,0.22836 0.0212,1.28159 0.0212,1.28159 l 25.61911,-4e-4 5.08082,5.101302 v 1.297938 l -2.42259,2.31776 -28.29811,-0.004 z"
|
||||
style="opacity:0.46400003;fill:#f90000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4725-7" />
|
||||
id="outline" />
|
||||
<path
|
||||
style="fill:#0030f9;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.03779528;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1;opacity:0.472"
|
||||
d="M 576.96875 354.61328 L 576.96875 371.18359 C 578.20685 371.714 579.60364 372.31821 580.26562 372.62891 C 581.55849 373.23568 581.62364 373.80632 580.82812 375.09766 C 580.18814 376.13654 578.00578 379.75309 576.96875 381.4707 L 576.96875 387.56055 L 584.1875 387.56055 L 584.1875 384.4043 C 583.33028 383.98907 582.38134 383.53251 582.02344 383.33984 C 581.04273 382.81188 580.87585 381.73877 581.3125 380.97266 C 581.59346 380.47971 582.96133 378.12124 584.1875 376.00586 L 584.1875 369.41406 L 580.82812 368.36328 L 580.82812 354.61328 L 576.96875 354.61328 z "
|
||||
style="opacity:0.472;fill:#0030f9;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.03779528;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 576.96875,354.61328 v 16.57031 c 1.2381,0.53041 2.63489,1.13462 3.29687,1.44532 1.29287,0.60677 1.35802,1.17741 0.5625,2.46875 -0.63998,1.03888 -2.82234,4.65543 -3.85937,6.37304 v 6.08985 h 7.21875 v -3.15625 c -0.85722,-0.41523 -1.80616,-0.87179 -2.16406,-1.06446 -0.98071,-0.52796 -1.14759,-1.60107 -0.71094,-2.36718 0.28096,-0.49295 1.64883,-2.85142 2.875,-4.9668 v -6.5918 l -3.35938,-1.05078 v -13.75 z"
|
||||
transform="scale(0.26458333)"
|
||||
id="path922" />
|
||||
id="warding"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.48600003;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#22f900;fill-opacity:1;fill-rule:evenodd;stroke:#f7f7f7;stroke-width:0.18897638;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="M 356.67773 326.10547 L 356.67773 417.03125 C 357.14345 416.38945 357.43379 415.59087 357.67383 414.83203 L 357.67383 328.30469 C 357.4338 327.54586 357.14344 326.74726 356.67773 326.10547 z M 342.20898 329.54102 C 342.12663 329.83691 342.00399 330.10148 341.79883 330.30664 C 341.60923 330.49624 341.40879 330.63434 341.20508 330.74805 L 341.20508 412.39062 C 341.40875 412.50432 341.60926 412.64051 341.79883 412.83008 C 342.00399 413.03524 342.12663 413.29981 342.20898 413.5957 L 342.20898 329.54102 z M 362.50977 332.16406 L 362.50977 410.97266 C 363.27812 409.78013 363.07667 407.79765 363.51367 406.23633 L 363.51367 336.90039 C 363.07696 335.33918 363.278 333.35639 362.50977 332.16406 z M 336.28906 338.19531 C 336.15669 338.46273 335.97745 338.72176 335.73242 338.9668 C 335.58026 339.11906 335.43203 339.2253 335.28516 339.31836 L 335.28516 355.30078 C 335.58746 355.17841 335.92821 355.08913 336.28906 355.03125 L 336.28906 338.19531 z M 367.90625 340.55469 L 367.90625 402.58203 C 368.21805 402.33327 368.54636 402.04524 368.90234 401.68555 L 368.90234 341.45117 C 368.54636 341.09148 368.21805 340.80345 367.90625 340.55469 z M 381.95312 344.36328 L 381.95312 398.77344 C 382.34332 398.37119 382.65906 397.84057 382.68945 397.17773 C 382.702 396.90357 382.8049 396.65904 382.95703 396.44141 L 382.95703 346.69531 C 382.8049 346.47768 382.702 346.23314 382.68945 345.95898 C 382.65906 345.29614 382.34332 344.76552 381.95312 344.36328 z M 335.28516 387.83594 L 335.28516 403.81836 C 335.43203 403.91136 335.58026 404.01776 335.73242 404.16992 C 335.97745 404.41495 336.15669 404.67399 336.28906 404.94141 L 336.28906 388.10547 C 335.92821 388.04759 335.58746 387.95831 335.28516 387.83594 z "
|
||||
transform="scale(0.26458333)"
|
||||
id="path935-9" />
|
||||
id="emboss"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.51999996;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 101.012,85.660311 v 25.443359 h 0.26563 V 85.660311 Z m -3.703732,-0.01793 v 25.443359 h 0.265625 V 85.642382 Z m -1.438672,0.01654 v 25.443349 h 0.265625 V 85.658919 Z m -1.438671,-0.04134 V 111.06094 H 94.69655 V 85.617577 Z m -4.191993,0.0083 V 111.0692 h 0.265625 V 85.625845 Z m -1.549479,0.01087 v 25.443359 h 0.265625 V 85.636719 Z"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
18
sc4_polygons.scad
Normal file
18
sc4_polygons.scad
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user