├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bezier ├── bezier.go ├── bezier_test.go └── utils.go ├── binpacking ├── binpacking.go └── binpacking_test.go ├── build.sh ├── components ├── around.go ├── basic_edge.go ├── gear.go ├── repeat_edge.go └── xintercept.go ├── designs ├── box │ ├── bottom.hfd │ ├── box_front_access.hfd │ ├── box_no_lid.hfd │ ├── box_with_lid.hfd │ ├── front_flat_top.hfd │ ├── front_with_access.hfd │ ├── holiday_tea_box_gus.hfd │ ├── lid.hfd │ ├── nadia_display_box.hfd │ └── side_flat_top.hfd ├── candle_holder │ ├── votive_center_struts.hfd │ └── votive_side_struts.hfd ├── chess │ └── pawn.hfd ├── common │ ├── circle_with_center_rectangle.hfd │ ├── rectangle.hfd │ ├── rectangle_with_center_hole.hfd │ ├── screw_holes_pair.hfd │ ├── screw_holes_rect.hfd │ └── stem_with_topper.hfd ├── component_examples │ ├── around.hfd │ ├── gear.hfd │ ├── part_split.hfd │ ├── svg_connect.hfd │ └── xintercept.hfd ├── divided_box │ ├── divided_box_2.hfd │ ├── divided_box_3.hfd │ └── divided_box_4.hfd ├── drawer_organizers │ └── silverware.hfd ├── game_cabinet │ ├── back_panel.hfd │ ├── bottom.hfd │ ├── button.hfd │ ├── button_layout.hfd │ ├── cabinet.hfd │ ├── front_panel.hfd │ ├── joystick.hfd │ ├── joystick_panel.hfd │ ├── marquee_panel.hfd │ ├── screen_inset_panel.hfd │ ├── screen_panel.hfd │ ├── side_panel.hfd │ ├── speaker.hfd │ └── top_panel.hfd ├── house │ ├── floor.hfd │ ├── front.hfd │ ├── house.hfd │ ├── roof.hfd │ ├── side.hfd │ └── window.hfd ├── joints │ ├── dragon_joint_plug.hfd │ ├── dragon_joint_socket.hfd │ ├── finger_joint_holes.hfd │ ├── finger_joint_plug.hfd │ ├── finger_joint_socket.hfd │ ├── line.hfd │ ├── slot_joint.hfd │ ├── svg_edge.hfd │ ├── tab_joint_plug.hfd │ └── tab_joint_socket.hfd ├── shelf │ ├── four_sided_shelf.hfd │ ├── four_sided_shelf_with_divider.hfd │ ├── three_sided_shelf.hfd │ └── three_sided_shelf_with_divider.hfd ├── svg │ ├── candle_holder_gus.svg │ ├── candle_holder_nadia.svg │ ├── dala_horse.svg │ ├── dragon.svg │ ├── lathe_pawn.svg │ ├── line.svg │ ├── part_circle.svg │ ├── silverware.svg │ ├── silverware_bottom.svg │ ├── squiggle_line.svg │ ├── test1.svg │ └── votive_1.svg └── xmas_tree │ ├── gus.hfd │ ├── layer.hfd │ ├── nadia.hfd │ └── spacer.hfd ├── designs_rendered ├── around_000.svg ├── back_panel_000.svg ├── bottom_000.svg ├── box_front_access_000.svg ├── box_no_lid_000.svg ├── box_with_lid_000.svg ├── button_000.svg ├── button_layout_000.svg ├── cabinet_000.svg ├── cabinet_001.svg ├── cabinet_002.svg ├── cabinet_003.svg ├── cabinet_004.svg ├── cabinet_005.svg ├── cabinet_006.svg ├── circle_with_center_rectangle_000.svg ├── divided_box_2_000.svg ├── divided_box_2_001.svg ├── divided_box_2_002.svg ├── divided_box_2_003.svg ├── divided_box_2_004.svg ├── divided_box_2_005.svg ├── divided_box_2_006.svg ├── divided_box_2_007.svg ├── divided_box_3_000.svg ├── divided_box_3_001.svg ├── divided_box_3_002.svg ├── divided_box_3_003.svg ├── divided_box_3_004.svg ├── divided_box_3_005.svg ├── divided_box_4_000.svg ├── dragon_joint_plug_000.svg ├── dragon_joint_socket_000.svg ├── finger_joint_holes_000.svg ├── finger_joint_plug_000.svg ├── finger_joint_socket_000.svg ├── floor_000.svg ├── four_sided_shelf_000.svg ├── four_sided_shelf_with_divider_000.svg ├── front_000.svg ├── front_flat_top_000.svg ├── front_panel_000.svg ├── front_with_access_000.svg ├── gear_000.svg ├── gus_000.svg ├── holiday_tea_box_gus_000.svg ├── house_000.svg ├── joystick_000.svg ├── joystick_panel_000.svg ├── layer_000.svg ├── lid_000.svg ├── line_000.svg ├── marquee_panel_000.svg ├── nadia_000.svg ├── nadia_display_box_000.svg ├── part_split_000.svg ├── pawn_000.svg ├── rectangle_000.svg ├── rectangle_with_center_hole_000.svg ├── roof_000.svg ├── screen_inset_panel_000.svg ├── screen_panel_000.svg ├── screw_holes_pair_000.svg ├── screw_holes_rect_000.svg ├── side_000.svg ├── side_flat_top_000.svg ├── side_panel_000.svg ├── silverware_000.svg ├── silverware_001.svg ├── silverware_002.svg ├── silverware_003.svg ├── silverware_004.svg ├── silverware_005.svg ├── silverware_006.svg ├── silverware_007.svg ├── silverware_008.svg ├── silverware_009.svg ├── silverware_010.svg ├── silverware_011.svg ├── slot_joint_000.svg ├── spacer_000.svg ├── speaker_000.svg ├── stem_with_topper_000.svg ├── svg_connect_000.svg ├── svg_edge_000.svg ├── tab_joint_plug_000.svg ├── tab_joint_socket_000.svg ├── three_sided_shelf_000.svg ├── three_sided_shelf_with_divider_000.svg ├── top_panel_000.svg ├── votive_center_struts_000.svg ├── votive_center_struts_001.svg ├── votive_side_struts_000.svg ├── votive_side_struts_001.svg ├── window_000.svg └── xintercept_000.svg ├── docs ├── Makefile ├── README.md ├── make.bat └── source │ ├── _static │ ├── around.png │ ├── around_with_triangle.png │ ├── box_with_access.jpg │ ├── dragon_repeat.png │ ├── game_cab.jpg │ ├── hfd_logo1.png │ ├── pawn.jpg │ ├── pawn_outline.png │ ├── rotate_after.png │ ├── rotate_before.png │ ├── silverware.jpg │ ├── slice_after.png │ ├── slice_before.png │ ├── square.png │ ├── xintercept_dala_horse_1.png │ ├── xintercept_dala_horse_2.png │ ├── xintercept_dala_horse_3.png │ └── xmas_tree.jpg │ ├── components.rst │ ├── conf.py │ ├── contributing.rst │ ├── custom_components.rst │ ├── designs.rst │ ├── expressions.rst │ ├── getting_started.rst │ ├── index.rst │ ├── part_transformers.rst │ └── transforms.rst ├── dom ├── app_context.go ├── common.go ├── document.go ├── draw_component.go ├── expression_eval.go ├── expression_eval_test.go ├── group_component.go ├── lathe_part_transformer.go ├── models.go ├── param_lookup.go ├── param_lookup_test.go ├── part.go ├── part_splitter.go ├── part_transform_factories.go ├── planset.go ├── render_context.go ├── repeat_component.go ├── svg_renderer.go └── transform_factories.go ├── dynmap ├── dynmap.go ├── dynmap_test.go ├── sort.go ├── sort_test.go └── typecast.go ├── go.mod ├── go.sum ├── main.go ├── parser ├── parser.go ├── parser_test.go ├── svgparser.go └── svgparser_test.go ├── path ├── attributes.go ├── attributes_test.go ├── draw.go ├── draw_test.go ├── path.go ├── path_parser.go ├── path_parser_test.go ├── path_test.go ├── segment_operators.go ├── segment_operators_test.go ├── transforms.go ├── util.go └── util_test.go ├── transforms ├── cleanup.go ├── cleanup_test.go ├── dedup.go ├── dedup_test.go ├── join.go ├── join_test.go ├── matrix.go ├── matrix_test.go ├── mirror.go ├── mirror_test.go ├── move.go ├── move_test.go ├── offset.go ├── offset_test.go ├── path_reorder.go ├── path_reorder_test.go ├── rebuild.go ├── reverse.go ├── reverse_test.go ├── rotate.go ├── rotate_scale.go ├── rotate_test.go ├── scale.go ├── scale_test.go ├── shift.go ├── slice.go ├── slice_test.go ├── trim_whitespace.go └── trim_whitespace_test.go └── util ├── file.go └── logging.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .DS_Store 14 | 15 | # doc build 16 | docs/build/ 17 | 18 | # vscode file 19 | *.code-workspace -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.inferGopath": false, 3 | "restructuredtext.confPath": "${workspaceFolder}/docs/source" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dustin Norlander 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is Heavy Fish Design 2 | ========================== 3 | 4 | It's for designing things that scale (get it, get it?). 5 | 6 | At its heart, HFD is a json based design language. This package contains a compiler to SVG files, and various useful tools for working with 2d models. 7 | 8 | Visit [heavyfishdesign.com](http://heavyfishdesign.com) for full documentation 9 | 10 | ## Features: 11 | 12 | * A fully functional JSON based language for designing parameterized 2d vector drawings 13 | * Set of useful transformations including joining, scaling, simplify and more 14 | * Outputs clear and concise svg 15 | * Layout engine will arrange parts to fit within the specified material size 16 | * Parts can be automatically split to fit within the cut material. 17 | 18 | ## Usage: 19 | 20 | 21 | The runnable contains a simple server for displaying in the browser, as well as commands for rendering designs on the command line. 22 | 23 | ### Local Render: 24 | 25 | To render an hfd file to the corresponding svg files use: 26 | 27 | $ go run main.go render --path=designs/drawer_organizers/silverware.hfd --output_file=designs_rendered/silverware 28 | 29 | ### Basic server operation: 30 | 31 | To run a local server to see svg's rendered in the browser, do this. This is useful to use during design, but note that by default, the server only displays the first rendered svg document (i.e. if your document spans multiple pages only the first is desplayed) 32 | 33 | go run main.go serve 34 | 35 | then open browser to: 36 | 37 | http://localhost:2003/json?file= 38 | 39 | ex: 40 | http://localhost:2003/json?file=designs/box/box_three_sided.hfd 41 | 42 | ***** 43 | 44 | Runbook 45 | ======== 46 | 47 | 48 | When making code changes, first run the unit tests then the comparison of all local designs (this will compare all the local designs to the last render) 49 | 50 | $ go test ./... 51 | $ go run main.go diff_test 52 | 53 | This will output any rendering differences your change causes. Double check that changes are expected. if so, run: 54 | 55 | $ go run main.go designs_updated 56 | 57 | and commit 58 | 59 | Build the docs: 60 | 61 | $ cd docs && make html && cd .. 62 | 63 | 64 | VSCODE 65 | 66 | To set the .hfd file associations edit settings.hfd and add: 67 | 68 | "files.associations": { 69 | "*.hfd": "json" 70 | } 71 | 72 | RELEASE 73 | 74 | To create a release 75 | 76 | $ git tag v1.0.X 77 | $ git push origin master --tags 78 | 79 | -------------------------------------------------------------------------------- /bezier/utils.go: -------------------------------------------------------------------------------- 1 | package bezier 2 | 3 | import "math" 4 | 5 | const MaxInt = int(^uint(0) >> 1) 6 | const MinInt = -MaxInt - 1 7 | 8 | // Distance finds the straightline distance between the two points 9 | func Distance(p1 Point, p2 Point) float64 { 10 | x := p1.X - p2.X 11 | y := p1.Y - p2.Y 12 | return math.Sqrt((x * x) + (y * y)) 13 | } 14 | 15 | // removes any duplicates from the array 16 | // order may or may not be maintained 17 | func Float64ArrayDeDup(a []float64) []float64 { 18 | keys := make(map[float64]bool) 19 | list := []float64{} 20 | for _, entry := range a { 21 | if _, value := keys[entry]; !value { 22 | keys[entry] = true 23 | list = append(list, entry) 24 | } 25 | } 26 | return list 27 | } 28 | 29 | // removes any duplicates maintaining ordering 30 | func StringArrayDeDup(a []string) []string { 31 | keys := make(map[string]bool) 32 | list := []string{} 33 | for _, entry := range a { 34 | if _, value := keys[entry]; !value { 35 | keys[entry] = true 36 | list = append(list, entry) 37 | } 38 | } 39 | return list 40 | } 41 | 42 | // inserts the requested item if it does not already exist 43 | func Float64ArrayInsertIfAbsent(a []float64, x float64) []float64 { 44 | if !Float64ArrayContains(a, x) { 45 | return append(a, x) 46 | } 47 | return a 48 | } 49 | 50 | func Float64ArrayContains(a []float64, x float64) bool { 51 | for _, n := range a { 52 | if x == n { 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | 59 | // returns true if requested is between v1 and v2 60 | // order of v1 and v2 does not matter 61 | func between(requested, v1, v2 float64) bool { 62 | a := v1 63 | b := v2 64 | r := requested 65 | if Approx(r, a) || Approx(r, b) { 66 | return true 67 | } 68 | 69 | return (r >= a && r <= b) || (r <= a && r >= b) 70 | } 71 | -------------------------------------------------------------------------------- /binpacking/binpacking_test.go: -------------------------------------------------------------------------------- 1 | package binpacking 2 | 3 | import "testing" 4 | 5 | // a bin for testing 6 | type MockBin struct { 7 | Width float64 8 | Height float64 9 | } 10 | 11 | func (m MockBin) GetWidth() float64 { 12 | return m.Width 13 | } 14 | 15 | func (m MockBin) GetHeight() float64 { 16 | return m.Height 17 | } 18 | 19 | func TestBinPacking(t *testing.T) { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This simple script for doing a production release 3 | # Assumes the docs site is in a sibling directory called heavyfishdesign-docs 4 | 5 | # echo Running tests 6 | go test ./... 7 | 8 | read -p "Continue? " -n 1 -r 9 | echo # (optional) move to a new line 10 | if [[ ! $REPLY =~ ^[Yy]$ ]] 11 | then 12 | exit 1 13 | fi 14 | 15 | # echo Running diff_Test 16 | 17 | go run main.go diff_test 18 | 19 | read -p "Continue? " -n 1 -r 20 | echo # (optional) move to a new line 21 | if [[ ! $REPLY =~ ^[Yy]$ ]] 22 | then 23 | exit 1 24 | fi 25 | 26 | # echo Updating designs 27 | 28 | go run main.go designs_updated 29 | 30 | # echo Rendering Documentation 31 | 32 | cd docs && make html && cd .. 33 | 34 | # now create bundle 35 | mkdir build_tmp 36 | mkdir build_tmp/heavyfishdesign 37 | 38 | echo Building windows binaries 39 | GOOS=windows GOARCH=amd64 go build main.go 40 | mv main.exe build_tmp/heavyfishdesign/hfd-windows.exe 41 | 42 | echo Building mac binaries 43 | GOOS=darwin GOARCH=amd64 go build main.go 44 | mv main build_tmp/heavyfishdesign/hfd-mac 45 | 46 | cp -R designs build_tmp/heavyfishdesign/designs 47 | 48 | cd build_tmp 49 | zip -r heavyfishdesign.zip heavyfishdesign 50 | mv heavyfishdesign.zip ../../heavyfishdesign-docs/html/_static 51 | cd .. 52 | rm -R build_tmp 53 | 54 | echo All Done! -------------------------------------------------------------------------------- /designs/box/bottom.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_holes.hfd"}, 4 | {"path": "designs/joints/finger_joint_plug.hfd"}, 5 | {"path": "designs/joints/finger_joint_socket.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "front_width": 5, 13 | "side_width": 6, 14 | "finger_width": 0.3, 15 | "finger_height": 0.2, 16 | "finger_space": 0.2, 17 | "finger_depth": "material_thickness" 18 | }, 19 | "parts": [ 20 | { 21 | "custom_component": { 22 | "type" : "box_bottom" 23 | }, 24 | "id" : "box_bottom", 25 | "finger_padding": "(finger_space + finger_width) / 2", 26 | "transforms": [ 27 | { 28 | "type": "join", 29 | "close_path" : "true" 30 | }, 31 | { 32 | "type": "offset", 33 | "distance" : "offset", 34 | "size_should_be" : "larger" 35 | } 36 | ], 37 | "components": [ 38 | { 39 | "type": "finger_joint_socket", 40 | "from": "0,0", 41 | "to" : "front_width,0" 42 | }, 43 | { 44 | "type": "finger_joint_socket", 45 | "from": "front_width,0", 46 | "to" : "front_width,side_width" 47 | }, 48 | { 49 | "type": "finger_joint_socket", 50 | "from": "front_width,side_width", 51 | "to" : "0,side_width" 52 | }, 53 | { 54 | "type": "finger_joint_socket", 55 | "from": "0,side_width", 56 | "to" : "0,0" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /designs/box/box_front_access.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/box/front_with_access.hfd"}, 4 | {"path": "designs/box/front_flat_top.hfd"}, 5 | {"path": "designs/box/bottom.hfd"}, 6 | {"path": "designs/box/side_flat_top.hfd"} 7 | ], 8 | "params": { 9 | "offset": ".0035", 10 | "material_width": 18, 11 | "material_height": 11, 12 | "material_thickness": 0.2, 13 | "box_width": 3, 14 | "box_height": 3.25, 15 | "box_depth": 1.6, 16 | "finger_width": 0.2, 17 | "finger_height": "material_thickness", 18 | "finger_space": 0.2, 19 | "finger_depth": "material_thickness", 20 | "finger_padding" : "(finger_space + finger_width) / 2", 21 | "cutout_height": "2.25", 22 | "cutout_width": ".8", 23 | "cutout_radius": ".4" 24 | }, 25 | "parts": [ 26 | { 27 | "id": "box_front", 28 | "components": [ 29 | { 30 | "type": "front_with_access", 31 | "width": "box_width", 32 | "height": "box_height" 33 | } 34 | ] 35 | }, 36 | { 37 | "id": "box_back", 38 | "components": [ 39 | { 40 | "type": "front_flat_top", 41 | "width": "box_width", 42 | "height": "box_height" 43 | } 44 | ] 45 | }, 46 | { 47 | "id": "side", 48 | "repeat": { 49 | "total": 2 50 | }, 51 | "components": [ 52 | { 53 | "type": "side_flat_top", 54 | "width": "box_depth", 55 | "height": "box_height" 56 | } 57 | ] 58 | }, 59 | { 60 | "id": "bottom", 61 | "components": [ 62 | { 63 | "type": "box_bottom", 64 | "front_width": "box_width", 65 | "side_width": "box_depth" 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /designs/box/box_no_lid.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/box/front_flat_top.hfd"}, 4 | {"path": "designs/box/bottom.hfd"}, 5 | {"path": "designs/box/side_flat_top.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "box_width": 2, 13 | "box_height": 3, 14 | "box_depth": 2, 15 | "finger_width": 0.2, 16 | "finger_height": "material_thickness", 17 | "finger_space": 0.2, 18 | "finger_depth": "material_thickness", 19 | "finger_padding" : "(finger_space + finger_width) / 2" 20 | }, 21 | "parts": [ 22 | { 23 | "id": "front_flat_top", 24 | "repeat": { 25 | "total": 2 26 | }, 27 | "components" : [{ 28 | "type": "front_flat_top", 29 | "width": "box_width", 30 | "height": "box_height" 31 | }] 32 | }, 33 | { 34 | "id": "side_flat_top", 35 | "repeat": { 36 | "total": 2 37 | }, 38 | "components" : [{ 39 | "type": "side_flat_top", 40 | "width": "box_depth", 41 | "height": "box_height" 42 | }] 43 | }, 44 | { 45 | "id": "bottom", 46 | "components" : [{ 47 | "type": "box_bottom", 48 | "front_width": "box_width", 49 | "side_width": "box_depth" 50 | }] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /designs/box/front_flat_top.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_plug.hfd"}, 4 | {"path": "designs/joints/line.hfd"} 5 | ], 6 | "params": { 7 | "offset": ".0035", 8 | "material_width": 20, 9 | "material_height": 12, 10 | "material_thickness": 0.2, 11 | "width": 5, 12 | "height": 6, 13 | "finger_width": 0.3, 14 | "finger_height": 0.2, 15 | "finger_space": 0.2, 16 | "finger_depth": "material_thickness" 17 | }, 18 | "parts": [ 19 | { 20 | "id" : "box_side", 21 | "custom_component" : { 22 | "type": "front_flat_top" 23 | }, 24 | "finger_padding": "(finger_space + finger_width) / 2", 25 | "transforms": [ 26 | { 27 | "type": "join", 28 | "close_path" : "true" 29 | }, 30 | { 31 | "type": "offset", 32 | "distance" : "offset", 33 | "size_should_be" : "larger" 34 | } 35 | ], 36 | "components": [ 37 | { 38 | "type" : "line", 39 | "from" : "0,0", 40 | "to" : "width,0" 41 | }, 42 | { 43 | "type": "finger_joint_plug", 44 | "padding_left": "finger_padding", 45 | "padding_right": "finger_padding + material_thickness", 46 | "from": "width,0", 47 | "to" : "width,height" 48 | }, 49 | { 50 | "type": "finger_joint_plug", 51 | "from" : "width,height", 52 | "to" : "0,height" 53 | }, 54 | { 55 | "type": "finger_joint_plug", 56 | "padding_right": "finger_padding", 57 | "padding_left": "finger_padding + material_thickness", 58 | "from": "0,height", 59 | "to" : "0,0" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /designs/box/holiday_tea_box_gus.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "filename": "designs/box/box_with_lid.hfd", 3 | "params" : { 4 | "stem_topper_svg": "M 12.087 19.347 L 11.101 19.347 C 11.101 19.191 11.016 18.916 10.920 18.824 C 10.775 18.687 10.684 18.739 10.648 18.630 C 10.556 18.604 10.467 18.635 10.380 18.696 C 10.254 18.601 10.216 18.485 10.262 18.351 L 10.326 18.295 L 10.728 18.231 L 10.917 18.311 L 10.994 18.131 L 10.834 18.018 L 10.597 17.818 L 10.661 17.607 C 10.561 17.657 10.498 17.130 10.567 17.147 C 10.654 17.168 11.178 17.854 11.178 17.854 L 11.278 17.674 L 11.544 17.257 L 11.652 17.294 L 11.702 17.351 L 11.724 17.771 L 11.892 17.849 L 11.918 17.752 C 11.908 16.790 12.051 17.089 12.110 17.248 L 12.310 17.674 L 12.373 17.297 L 11.824 16.702 L 12.131 16.775 C 12.131 16.775 12.405 16.524 12.523 16.533 C 12.640 16.543 12.810 16.804 12.810 16.804 L 13.210 16.722 L 12.858 17.107 L 12.824 17.215 L 12.893 17.785 L 13.020 17.604 C 13.020 17.604 13.077 17.127 13.131 17.060 C 13.300 16.850 13.335 17.316 13.275 17.383 L 13.250 17.566 L 13.320 17.674 L 13.280 17.906 L 13.420 18.018 C 13.420 18.018 13.419 17.200 13.468 17.091 C 13.518 16.982 13.715 17.364 13.715 17.364 L 13.763 17.429 L 13.843 18.018 C 13.843 18.018 14.636 17.409 14.744 17.392 C 14.851 17.375 14.489 17.914 14.489 17.914 L 14.088 18.235 L 14.135 18.406 L 14.443 18.294 L 14.560 18.290 C 14.560 18.290 15.189 17.764 15.238 17.831 C 15.287 17.898 14.971 18.808 14.971 18.808 L 14.537 18.715 L 14.064 18.930 L 14.051 19.347 L 13.022 19.347", 5 | "box_width": 5.5, 6 | "box_height": 3.75, 7 | "box_depth": 3.25, 8 | "handle_hole": 0.4, 9 | "finger_width": 0.2, 10 | "finger_space": 0.2 11 | } 12 | } -------------------------------------------------------------------------------- /designs/box/lid.hfd: -------------------------------------------------------------------------------- 1 | // creates a two layered lid, where the inner layer fits within the box, 2 | // and has a hole to connect both via a handle 3 | { 4 | "imports": [ 5 | {"path": "designs/common/rectangle_with_center_hole.hfd"}, 6 | {"path": "designs/common/stem_with_topper.hfd"} 7 | ], 8 | "params": { 9 | "offset": ".0035", 10 | "material_width": 20, 11 | "material_height": 12, 12 | "material_thickness": 0.2, 13 | "front_width": 5, 14 | "side_width": 6, 15 | "handle_hole": 0.3, 16 | "stem_topper_svg": "M 425.474 705.105 L 345.318 705.105 C 330.065 705.105 317.682 692.721 317.682 677.468 L 317.682 622.196 C 317.682 606.943 330.065 594.559 345.318 594.559 C 383.050 626.859 417.826 640.878 450.386 641.197 C 482.947 640.878 517.723 626.859 555.455 594.559 C 570.708 594.559 583.091 606.943 583.091 622.196 L 583.091 677.468 C 583.091 692.721 570.708 705.105 555.455 705.105 L 475.298 705.105", 17 | "finger_width": 0.3, 18 | "finger_height": 0.2, 19 | "finger_space": 0.2, 20 | "finger_depth": "material_thickness" 21 | }, 22 | "parts": [ 23 | { 24 | "id": "inner_rectangle", 25 | "custom_component": { 26 | "type" : "lid_underside" 27 | }, 28 | "components": [ 29 | { 30 | "type": "rectangle_with_center_hole", 31 | "hole_width" : "handle_hole", 32 | "hole_height" : "material_thickness", 33 | "width": "front_width - (material_thickness * 2) - (material_thickness * .1)", 34 | "height": "side_width - (material_thickness * 2) - (material_thickness * .1)" 35 | } 36 | ] 37 | }, 38 | { 39 | "id": "outer_rectangle", 40 | "custom_component": { 41 | "type" : "lid_top" 42 | }, 43 | "components": [ 44 | { 45 | "type": "rectangle_with_center_hole", 46 | "hole_width" : "handle_hole", 47 | "hole_height" : "material_thickness", 48 | "width": "front_width", 49 | "height": "side_width" 50 | } 51 | ] 52 | }, 53 | { 54 | "id": "handle", 55 | "custom_component": { 56 | "type" : "lid_handle" 57 | }, 58 | "components": [ 59 | { 60 | "id" : "lid_handle", 61 | "type": "stem_with_topper", 62 | "stem_width" : "handle_hole", 63 | "stem_height" : "material_thickness * 2" 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /designs/box/side_flat_top.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_plug.hfd"}, 4 | {"path": "designs/joints/finger_joint_socket.hfd"}, 5 | {"path": "designs/joints/line.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "width": 5, 13 | "height": 6, 14 | "finger_width": 0.3, 15 | "finger_height": 0.2, 16 | "finger_space": 0.2, 17 | "finger_depth": "material_thickness" 18 | }, 19 | "parts": [ 20 | { 21 | "id" : "box_side", 22 | "custom_component": { 23 | "type": "side_flat_top" 24 | }, 25 | "finger_padding": "(finger_space + finger_width) / 2", 26 | "transforms": [ 27 | { 28 | "type": "join", 29 | "close_path" : "true" 30 | }, 31 | { 32 | "type": "offset", 33 | "distance" : "offset", 34 | "size_should_be" : "larger" 35 | } 36 | ], 37 | "components": [ 38 | { 39 | "type" : "line", 40 | "from" : "0,0", 41 | "to": "width,0" 42 | }, 43 | { 44 | "type": "finger_joint_socket", 45 | "padding_left": "finger_padding", 46 | "padding_right": "finger_padding + material_thickness", 47 | "from": "width,0", 48 | "to" : "width,height" 49 | }, 50 | { 51 | "type": "finger_joint_plug", 52 | "from" : "width,height", 53 | "to" : "0,height" 54 | }, 55 | { 56 | "type": "finger_joint_socket", 57 | "padding_right": "finger_padding", 58 | "padding_left": "finger_padding + material_thickness", 59 | "from": "0,height", 60 | "to" : "0,0" 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /designs/chess/pawn.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/common/rectangle_with_center_hole.hfd"}, 4 | {"path": "designs/common/rectangle.hfd"}, 5 | { 6 | "path": "designs/svg/lathe_pawn.svg", 7 | "type": "svg", 8 | "alias": "pawn" 9 | } 10 | ], 11 | "params": { 12 | "offset": ".0035", 13 | "material_width": 20, 14 | "material_height": 12, 15 | "material_thickness": 0.2, 16 | "pawn_height": 2.8 17 | }, 18 | "parts": [ 19 | { 20 | "label": { 21 | "text": "pwn" 22 | }, 23 | "part_transformers": [ 24 | { 25 | "type" : "lathe", 26 | "lathe_variable_name": "lathe_pawn", 27 | "repeat": { 28 | "type": "rectangle_with_center_hole", 29 | "width": "lathe__width", 30 | "height": "lathe__width", 31 | "hole_width": "material_thickness", 32 | "hole_height": "material_thickness" 33 | } 34 | } 35 | ], 36 | "components": [ 37 | { 38 | "transforms": [ 39 | { 40 | "type": "scale", 41 | "height": "pawn_height" 42 | } 43 | ], 44 | "type": "draw", 45 | "commands": [ 46 | { 47 | "command": "svg", 48 | "svg": "pawn" 49 | } 50 | ] 51 | } 52 | ] 53 | }, 54 | { 55 | "components": [ 56 | { 57 | "id": "stem", 58 | "type": "rectangle", 59 | "width": "material_thickness", 60 | "height": "lathe_pawn__total_height - material_thickness" 61 | } 62 | ] 63 | }, 64 | { 65 | "components": [ 66 | { 67 | "id": "pawn_topper", 68 | "type": "rectangle", 69 | "width": "lathe_pawn__top_width", 70 | "height": "lathe_pawn__top_width" 71 | } 72 | ] 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /designs/common/circle_with_center_rectangle.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "offset": ".0035", 4 | "material_width": 20, 5 | "material_height": 12, 6 | "material_thickness": 0.2, 7 | "radius": 5, 8 | "hole_width": 0.5, 9 | "hole_height": "material_thickness" 10 | }, 11 | "parts": [ 12 | { 13 | "id" : "circle_with_center_rectangle", 14 | "custom_component": { 15 | "type": "circle_with_center_rectangle" 16 | }, 17 | "components": [ 18 | { 19 | "transforms": [ 20 | { 21 | "type": "offset", 22 | "distance" : "offset", 23 | "size_should_be" : "larger" 24 | } 25 | ], 26 | "type" : "draw", 27 | "commands" : [ 28 | { 29 | "command" : "move", 30 | "to" : {"x": "0", "y": "0"} 31 | }, 32 | { 33 | "command" : "circle", 34 | "radius" : "radius" 35 | } 36 | ] 37 | }, 38 | { 39 | "type": "draw", 40 | "id": "handle_hole", 41 | "transforms" : [ 42 | { 43 | "type": "offset", 44 | "distance" : "offset", 45 | "size_should_be" : "smaller" 46 | }, 47 | { 48 | "type": "move", 49 | "to": { 50 | "x" : "radius", 51 | "y" : "radius" 52 | }, 53 | "handle": "$MIDDLE_MIDDLE" 54 | } 55 | ], 56 | "commands": [ 57 | { 58 | "command": "rectangle", 59 | "width": "hole_width", 60 | "height": "hole_height" 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /designs/common/rectangle.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "width": "2", 4 | "height": "2" 5 | }, 6 | "parts": [ 7 | { 8 | "components": [ 9 | { 10 | "type": "draw", 11 | "custom_component": { 12 | "type": "rectangle" 13 | }, 14 | "commands": [ 15 | { 16 | "command": "rectangle", 17 | "width": "width", 18 | "height": "height" 19 | } 20 | ] 21 | } 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /designs/common/rectangle_with_center_hole.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "offset": ".0035", 4 | "material_width": 20, 5 | "material_height": 12, 6 | "material_thickness": 0.2, 7 | "width": 5, 8 | "height": 6, 9 | "hole_width": 0.5, 10 | "hole_height": "material_thickness" 11 | }, 12 | "parts": [ 13 | { 14 | "id" : "rectangle_with_center_hole", 15 | "custom_component": { 16 | "type": "rectangle_with_center_hole" 17 | }, 18 | "components": [ 19 | { 20 | "transforms": [ 21 | { 22 | "type": "offset", 23 | "distance" : "offset", 24 | "size_should_be" : "larger" 25 | } 26 | ], 27 | "type" : "draw", 28 | "commands" : [ 29 | { 30 | "command" : "move", 31 | "to" : {"x": "0", "y": "0"} 32 | }, 33 | { 34 | "command" : "rectangle", 35 | "width" : "width", 36 | "height" : "height" 37 | } 38 | ] 39 | }, 40 | { 41 | "type": "draw", 42 | "id": "handle_hole", 43 | "transforms" : [ 44 | { 45 | "type": "offset", 46 | "distance" : "offset", 47 | "size_should_be" : "smaller" 48 | }, 49 | { 50 | "type": "move", 51 | "to": { 52 | "x" : "width / 2", 53 | "y" : "height / 2" 54 | }, 55 | "handle": "$MIDDLE_MIDDLE" 56 | } 57 | ], 58 | "commands": [ 59 | { 60 | "command": "rectangle", 61 | "width": "hole_width", 62 | "height": "hole_height" 63 | } 64 | ] 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /designs/common/screw_holes_pair.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "width": "2", // width from outer edge to outer edge of the screw holes 4 | "diameter": ".2" // the diameter of the screw holes 5 | }, 6 | "parts": [ 7 | { 8 | "components": [ 9 | { 10 | "type": "draw", 11 | "custom_component": { 12 | "type": "screw_holes_pair" 13 | }, 14 | "commands": [ 15 | { 16 | "command": "move", 17 | "to": "0,0" 18 | }, 19 | { 20 | "command": "circle", 21 | "radius": "diameter / 2" 22 | }, 23 | { 24 | "command": "move", 25 | "to": "width - diameter, 0" 26 | }, 27 | { 28 | "command": "circle", 29 | "radius": "diameter / 2" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /designs/common/screw_holes_rect.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "width": "2", // width from outer edge to outer edge of the screw holes 4 | "height": "2", // height from outer edges of screw holes 5 | "diameter": ".225" // the diameter of the screw holes 6 | }, 7 | "parts": [ 8 | { 9 | "components": [ 10 | { 11 | "type": "draw", 12 | "custom_component": { 13 | "type": "screw_holes_rect" 14 | }, 15 | "commands": [ 16 | { 17 | "command": "move", 18 | "to": "0,0" 19 | }, 20 | { 21 | "command": "circle", 22 | "radius": "diameter / 2" 23 | }, 24 | { 25 | "command": "move", 26 | "to": "width - diameter, 0" 27 | }, 28 | { 29 | "command": "circle", 30 | "radius": "diameter / 2" 31 | }, 32 | { 33 | "command": "move", 34 | "to": "width - diameter, height - diameter" 35 | }, 36 | { 37 | "command": "circle", 38 | "radius": "diameter / 2" 39 | }, 40 | { 41 | "command": "move", 42 | "to": "0, height - diameter" 43 | }, 44 | { 45 | "command": "circle", 46 | "radius": "diameter / 2" 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /designs/common/stem_with_topper.hfd: -------------------------------------------------------------------------------- 1 | // a rectangle with some svg as a topper 2 | { 3 | "params": { 4 | "stem_height": 10, 5 | "stem_width": 0.5, 6 | "offset": 0.0035, 7 | // "svg": "M348.7,980.332L257.96,1029.19L329.328,881.936C281.795,865.185 237.209,843.574 195.153,817.789C195.153,817.789 299.955,797.109 342.386,807.735C370.154,814.688 392.808,596.463 392.808,596.463C392.808,596.463 458.914,737.584 458.863,787.27C458.836,814.165 630.607,753.163 635.789,762.13C645.634,779.165 488.991,864.122 495.723,875.519C502.467,886.936 543.134,961.332 590.87,1013.95C638.607,1066.56 477.01,980.332 477.01,980.332" 8 | "stem_topper_svg": "M 425.474 705.105 L 345.318 705.105 C 330.065 705.105 317.682 692.721 317.682 677.468 L 317.682 622.196 C 317.682 606.943 330.065 594.559 345.318 594.559 C 383.050 626.859 417.826 640.878 450.386 641.197 C 482.947 640.878 517.723 626.859 555.455 594.559 C 570.708 594.559 583.091 606.943 583.091 622.196 L 583.091 677.468 C 583.091 692.721 570.708 705.105 555.455 705.105 L 475.298 705.105" 9 | }, 10 | "parts": [ 11 | { 12 | "custom_component": { 13 | "type": "stem_with_topper" 14 | }, 15 | "components": [ 16 | { 17 | "type": "draw", 18 | "id": "stem", 19 | "transforms" : [ 20 | { 21 | "type": "join" 22 | } 23 | ], 24 | "commands": [ 25 | { 26 | "command": "move", 27 | "to": "stem_width + (offset * 2),0" 28 | }, 29 | { 30 | "command": "line", 31 | "to": "stem_width + (offset * 2),stem_height" 32 | }, 33 | { 34 | "command": "line", 35 | "to": "0,stem_height" 36 | }, 37 | { 38 | "command": "line", 39 | "to": "0,0" 40 | }, 41 | { 42 | "command": "svg_scale_to", 43 | "svg": "stem_topper_svg", 44 | "reverse": "false", 45 | "to": "stem_width + (offset * 2),0" 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /designs/component_examples/around.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/common/rectangle.hfd"} 4 | ], 5 | "params": { 6 | "offset": ".0035", 7 | "material_width": 20, 8 | "material_height": 12, 9 | "around_radius": 2, 10 | "around_num_edges": 9 11 | }, 12 | "parts": [ 13 | { 14 | "components": [ 15 | { 16 | "type" : "around", 17 | "num_edges": "around_num_edges", 18 | "radius": "around_radius", 19 | "repeatable": { 20 | "type": "draw", 21 | "transforms": [ 22 | // move to the center of the length 23 | { 24 | "type" : "move", 25 | "to" : "around__length / 2, 0", 26 | "handle" : "$BOTTOM_MIDDLE" 27 | } 28 | ], 29 | "commands": [ 30 | { 31 | "command": "rectangle", 32 | "width": "around__length - .3", 33 | "height": ".2" 34 | } 35 | ] 36 | } 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /designs/component_examples/gear.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [], 3 | "params": { 4 | "offset": ".0035", 5 | "material_width": 20, 6 | "material_height": 12, 7 | "tooth_size": 0.2, 8 | "hole_radius": "0.25 / 2", 9 | "num_teeth": 22, 10 | "num_gears": 1 11 | }, 12 | "parts": [ 13 | { 14 | "repeat": { 15 | "total": "num_gears" 16 | }, 17 | "components": [ 18 | { 19 | "transforms": [ 20 | // { 21 | // "type": "offset", 22 | // "distance": "offset", 23 | // "size_should_be": "larger", 24 | // "precision": 6 25 | // } 26 | ], 27 | "type": "gear", 28 | "teeth": "num_teeth", 29 | "tooth_width": "tooth_size", 30 | "gear_variable_name": "gear_25" 31 | }, 32 | { 33 | "type": "draw", 34 | "id": "handle_hole", 35 | "transforms": [ 36 | { 37 | "type": "offset", 38 | "distance": "offset", 39 | "size_should_be": "smaller" 40 | }, 41 | { 42 | "type": "move", 43 | "to": { 44 | "x": "0", 45 | "y": "0" 46 | }, 47 | "handle": "$MIDDLE_MIDDLE" 48 | } 49 | ], 50 | "commands": [ 51 | { 52 | "command": "circle", 53 | "radius": "hole_radius" 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /designs/component_examples/svg_connect.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/line.hfd"}, 4 | {"path": "designs/joints/finger_joint_plug.hfd"}, 5 | {"path": "designs/joints/finger_joint_socket.hfd"}, 6 | { 7 | "path": "designs/svg/squiggle_line.svg", 8 | "type": "svg", 9 | "alias": "squiggle_line" 10 | } 11 | ], 12 | "params": { 13 | "offset": ".0035", 14 | "material_width": 20, 15 | "material_height": 20, 16 | "material_thickness": 0.2, 17 | "box_width": 4, 18 | "box_front_height": 2, 19 | "box_back_height": 3, 20 | "box_depth": 3, 21 | "finger_width": 0.4, 22 | "finger_height": "material_thickness", 23 | "finger_space": 0.3, 24 | "finger_depth": "material_thickness", 25 | "finger_padding" : 0.2 26 | }, 27 | "parts": [ 28 | { 29 | "id": "side", 30 | "components": [ 31 | { 32 | "type" : "group", 33 | "transforms": [ 34 | { 35 | "type": "join", 36 | "close_path" : "true" 37 | }, 38 | { 39 | "type": "offset", 40 | "distance" : "offset", 41 | "size_should_be" : "larger" 42 | } 43 | ], 44 | "components": [ 45 | { 46 | "type": "draw", 47 | "commands": [ 48 | { 49 | "command": "svg_connect_to", 50 | "to": "box_depth,box_back_height-box_front_height", 51 | "svg": "squiggle_line" 52 | } 53 | 54 | ] 55 | }, 56 | { 57 | "type": "line", 58 | "from": "box_depth,box_back_height-box_front_height", 59 | "to" : "box_depth,box_back_height" 60 | }, 61 | { 62 | "type": "finger_joint_plug", 63 | "from": "box_depth,box_back_height", 64 | "to" : "0,box_back_height" 65 | }, 66 | { 67 | "type": "finger_joint_plug", 68 | "from": "0,box_back_height", 69 | "to" : "0,0" 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /designs/component_examples/xintercept.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | { 4 | "path": "designs/svg/dala_horse.svg", 5 | "type": "svg", 6 | "alias": "dala_horse" 7 | } 8 | ], 9 | "params": { 10 | "offset": ".0035", 11 | "material_width": 20, 12 | "material_height": 12, 13 | "slot_height": ".1", 14 | "slot_spacing": ".1" 15 | }, 16 | "parts": [ 17 | { 18 | "components": [ 19 | { 20 | "type" : "xintercept", 21 | "repeat_spacing": "slot_spacing", 22 | "outline": { 23 | "transforms": [ 24 | { 25 | "type": "scale", 26 | "width": "3" 27 | } 28 | ], 29 | "type": "draw", 30 | "commands": [ 31 | { 32 | "command": "svg", 33 | "svg": "dala_horse" 34 | } 35 | ] 36 | }, 37 | "repeatable": { 38 | "type": "draw", 39 | "transforms": [ 40 | { 41 | "type" : "move", 42 | "to": "xintercept__from__x, xintercept__from__y" 43 | } 44 | ], 45 | "commands": [ 46 | { 47 | "command": "rectangle", 48 | "width": "xintercept__length", 49 | "height": "slot_height" 50 | } 51 | ] 52 | } 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /designs/game_cabinet/back_panel.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_plug.hfd"}, 4 | {"path": "designs/joints/finger_joint_socket.hfd"}, 5 | {"path": "designs/joints/line.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "finger_width": 0.3, 13 | "finger_height": 0.2, 14 | "finger_space": 0.2, 15 | "finger_depth": "material_thickness", 16 | "width": 10, 17 | "height": 10 18 | }, 19 | "parts": [ 20 | { 21 | "custom_component" : { 22 | "type": "cabinet_back_panel" 23 | }, 24 | "transforms": [ 25 | { 26 | "type": "join", 27 | "close_path" : "true" 28 | }, 29 | { 30 | "type": "offset", 31 | "distance" : "offset", 32 | "size_should_be" : "larger" 33 | } 34 | ], 35 | "components": [ 36 | { 37 | "type" : "finger_joint_socket", 38 | "finger_height": "material_thickness * 1.1", // add a little height because this is joined at an angle 39 | "from" : "0,0", 40 | "to" : "width,0" 41 | }, 42 | { 43 | "type": "finger_joint_plug", 44 | "from": "width,0", 45 | "to" : "width,height" 46 | }, 47 | { 48 | "type": "finger_joint_plug", 49 | "from" : "width,height", 50 | "to" : "0,height" 51 | }, 52 | { 53 | "type": "finger_joint_plug", 54 | "from": "0,height", 55 | "to" : "0,0" 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /designs/game_cabinet/bottom.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_plug.hfd"}, 4 | {"path": "designs/joints/finger_joint_socket.hfd"}, 5 | {"path": "designs/joints/line.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "finger_width": 0.3, 13 | "finger_height": 0.2, 14 | "finger_space": 0.2, 15 | "finger_depth": "material_thickness", 16 | "width": 10, 17 | "height": 10 18 | }, 19 | "parts": [ 20 | { 21 | "custom_component" : { 22 | "type": "cabinet_bottom" 23 | }, 24 | "transforms": [ 25 | { 26 | "type": "join", 27 | "close_path" : "true" 28 | }, 29 | { 30 | "type": "offset", 31 | "distance" : "offset", 32 | "size_should_be" : "larger" 33 | } 34 | ], 35 | "components": [ 36 | { 37 | "id": "back", 38 | "type" : "finger_joint_socket", 39 | "from" : "0,0", 40 | "to" : "width,0" 41 | }, 42 | { 43 | "id": "right", 44 | "type": "finger_joint_plug", 45 | "from": "width,0", 46 | "to" : "width,height" 47 | }, 48 | { 49 | "id": "front", 50 | "type": "finger_joint_plug", 51 | "from" : "width,height", 52 | "to" : "0,height" 53 | }, 54 | { 55 | "type": "finger_joint_plug", 56 | "from": "0,height", 57 | "to" : "0,0" 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /designs/game_cabinet/button.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | ], 4 | "params": { 5 | "offset": ".0035", 6 | "material_width": 20, 7 | "material_height": 12, 8 | "material_thickness": 0.2, 9 | "diameter": 0.75, 10 | "to": "1,1", 11 | "handle": "$MIDDLE_LEFT" 12 | }, 13 | "parts": [ 14 | { 15 | "custom_component" : { 16 | "type": "button" 17 | }, 18 | "components": [ 19 | { 20 | "type": "draw", 21 | "transforms": [ 22 | { 23 | "type": "offset", 24 | "distance" : "offset", 25 | "size_should_be" : "smaller" 26 | }, 27 | { 28 | "type" : "move", 29 | "handle": "handle", 30 | "to": "to" 31 | } 32 | ], 33 | "commands": [ 34 | { 35 | "command": "circle", 36 | "radius": "diameter / 2" 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /designs/game_cabinet/front_panel.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_plug.hfd"}, 4 | {"path": "designs/joints/finger_joint_socket.hfd"}, 5 | {"path": "designs/joints/line.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "finger_width": 0.3, 13 | "finger_height": 0.2, 14 | "finger_space": 0.2, 15 | "finger_depth": "material_thickness", 16 | "width": 10, 17 | "height": 10 18 | }, 19 | "parts": [ 20 | { 21 | "custom_component" : { 22 | "type": "cabinet_front_panel" 23 | }, 24 | "transforms": [ 25 | { 26 | "type": "join", 27 | "close_path" : "true" 28 | }, 29 | { 30 | "type": "offset", 31 | "distance" : "offset", 32 | "size_should_be" : "larger" 33 | } 34 | ], 35 | "components": [ 36 | { 37 | "type" : "finger_joint_socket", 38 | "finger_height": "material_thickness * 1.1", // add a little height because this is joined at an angle 39 | "from" : "0,0", 40 | "to" : "width,0" 41 | }, 42 | { 43 | "type": "finger_joint_plug", 44 | "from": "width,0", 45 | "to" : "width,height" 46 | }, 47 | { 48 | "type": "finger_joint_socket", 49 | "from" : "width,height", 50 | "to" : "0,height" 51 | }, 52 | { 53 | "type": "finger_joint_plug", 54 | "from": "0,height", 55 | "to" : "0,0" 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /designs/game_cabinet/joystick.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | ], 4 | "params": { 5 | "offset": ".0035", 6 | "material_width": 20, 7 | "material_height": 12, 8 | "material_thickness": 0.2, 9 | "joystick_diameter": 0.75, 10 | "joystick_screw_diameter": 0.1, 11 | "joystick_screw_distance": 3 12 | }, 13 | "parts": [ 14 | { 15 | "custom_component" : { 16 | "type": "joystick" 17 | }, 18 | "components": [ 19 | { 20 | "type": "draw", 21 | "transforms": [ 22 | { 23 | "type": "offset", 24 | "distance" : "offset", 25 | "size_should_be" : "smaller" 26 | } 27 | ], 28 | "commands": [ 29 | { 30 | "command": "move", 31 | "to": "(joystick_diameter / 2) - (joystick_screw_diameter / 2), 0" 32 | }, 33 | { 34 | "command": "circle", 35 | "radius": "joystick_screw_diameter / 2" 36 | }, 37 | { 38 | "command": "move", 39 | "to": "0, (joystick_screw_distance / 2) - (joystick_diameter / 2) + (joystick_screw_diameter / 2)" 40 | }, 41 | { 42 | "command": "circle", 43 | "radius": "joystick_diameter / 2" 44 | }, 45 | { 46 | "command": "move", 47 | "to": "(joystick_diameter / 2) - (joystick_screw_diameter / 2), joystick_screw_distance" 48 | }, 49 | { 50 | "command": "circle", 51 | "radius": "joystick_screw_diameter / 2" 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /designs/game_cabinet/joystick_panel.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_plug.hfd"}, 4 | {"path": "designs/joints/finger_joint_socket.hfd"}, 5 | {"path": "designs/joints/line.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "finger_width": 0.3, 13 | "finger_height": 0.2, 14 | "finger_space": 0.2, 15 | "finger_depth": "material_thickness", 16 | "width": 10, 17 | "height": 10 18 | }, 19 | "parts": [ 20 | { 21 | "custom_component" : { 22 | "type": "cabinet_joystick_panel" 23 | }, 24 | "transforms": [ 25 | { 26 | "type": "join", 27 | "close_path" : "true" 28 | }, 29 | { 30 | "type": "offset", 31 | "distance" : "offset", 32 | "size_should_be" : "larger" 33 | } 34 | ], 35 | "components": [ 36 | { 37 | "type" : "line", 38 | "from" : "0,0", 39 | "to" : "width,0" 40 | }, 41 | { 42 | "type": "finger_joint_plug", 43 | "from": "width,0", 44 | "to" : "width,height" 45 | }, 46 | { 47 | "type": "finger_joint_plug", 48 | "finger_height": "material_thickness * 1.1", // add a little height because this is joined at an angle 49 | "from" : "width,height", 50 | "to" : "0,height" 51 | }, 52 | { 53 | "type": "finger_joint_plug", 54 | "from": "0,height", 55 | "to" : "0,0" 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /designs/game_cabinet/marquee_panel.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_plug.hfd"}, 4 | {"path": "designs/joints/finger_joint_socket.hfd"}, 5 | {"path": "designs/joints/line.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "finger_width": 0.3, 13 | "finger_height": 0.2, 14 | "finger_space": 0.2, 15 | "finger_depth": "material_thickness", 16 | "width": 10, 17 | "height": 10 18 | }, 19 | "parts": [ 20 | { 21 | "custom_component" : { 22 | "type": "cabinet_marquee_panel" 23 | }, 24 | "transforms": [ 25 | { 26 | "type": "join", 27 | "close_path" : "true" 28 | }, 29 | { 30 | "type": "offset", 31 | "distance" : "offset", 32 | "size_should_be" : "larger" 33 | } 34 | ], 35 | "components": [ 36 | { 37 | "type" : "finger_joint_socket", 38 | "from" : "0,0", 39 | "to" : "width,0" 40 | }, 41 | { 42 | "type": "finger_joint_plug", 43 | "from": "width,0", 44 | "to" : "width,height" 45 | }, 46 | { 47 | "type": "finger_joint_socket", 48 | "from" : "width,height", 49 | "to" : "0,height" 50 | }, 51 | { 52 | "type": "finger_joint_plug", 53 | "from": "0,height", 54 | "to" : "0,0" 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /designs/game_cabinet/screen_inset_panel.hfd: -------------------------------------------------------------------------------- 1 | // This is the panel that joins the marquee to the screen_backer 2 | { 3 | "imports": [ 4 | {"path": "designs/joints/finger_joint_plug.hfd"}, 5 | {"path": "designs/joints/finger_joint_socket.hfd"}, 6 | {"path": "designs/joints/line.hfd"} 7 | ], 8 | "params": { 9 | "offset": ".0035", 10 | "material_width": 20, 11 | "material_height": 12, 12 | "material_thickness": 0.2, 13 | "finger_width": 0.3, 14 | "finger_height": 0.2, 15 | "finger_space": 0.2, 16 | "finger_depth": "material_thickness", 17 | "width": 10, 18 | "height": 10 19 | }, 20 | "parts": [ 21 | { 22 | "custom_component" : { 23 | "type": "cabinet_screen_inset_panel" 24 | }, 25 | "transforms": [ 26 | { 27 | "type": "join", 28 | "close_path" : "true" 29 | }, 30 | { 31 | "type": "offset", 32 | "distance" : "offset", 33 | "size_should_be" : "larger" 34 | } 35 | ], 36 | "components": [ 37 | { 38 | "type" : "finger_joint_plug", 39 | "from" : "0,0", 40 | "to" : "width,0" 41 | }, 42 | { 43 | "type": "finger_joint_plug", 44 | "from": "width,0", 45 | "to" : "width,height" 46 | }, 47 | { 48 | "type": "line", 49 | "from" : "width,height", 50 | "to" : "0,height" 51 | }, 52 | { 53 | "type": "finger_joint_plug", 54 | "from": "0,height", 55 | "to" : "0,0" 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /designs/game_cabinet/top_panel.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_plug.hfd"}, 4 | {"path": "designs/joints/finger_joint_socket.hfd"}, 5 | {"path": "designs/joints/line.hfd"} 6 | ], 7 | "params": { 8 | "offset": ".0035", 9 | "material_width": 20, 10 | "material_height": 12, 11 | "material_thickness": 0.2, 12 | "finger_width": 0.3, 13 | "finger_height": 0.2, 14 | "finger_space": 0.2, 15 | "finger_depth": "material_thickness", 16 | "width": 10, 17 | "height": 10 18 | }, 19 | "parts": [ 20 | { 21 | "custom_component" : { 22 | "type": "cabinet_top_panel" 23 | }, 24 | "transforms": [ 25 | { 26 | "type": "join", 27 | "close_path" : "true" 28 | }, 29 | { 30 | "type": "offset", 31 | "distance" : "offset", 32 | "size_should_be" : "larger" 33 | } 34 | ], 35 | "components": [ 36 | { 37 | "type" : "finger_joint_plug", 38 | "finger_height": "material_thickness * 1.1", // add a little height because this is joined at an angle 39 | "from" : "0,0", 40 | "to" : "width,0" 41 | }, 42 | { 43 | "type": "finger_joint_plug", 44 | "from": "width,0", 45 | "to" : "width,height" 46 | }, 47 | { 48 | "type": "finger_joint_plug", 49 | "from" : "width,height", 50 | "to" : "0,height" 51 | }, 52 | { 53 | "type": "finger_joint_plug", 54 | "from": "0,height", 55 | "to" : "0,0" 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /designs/house/floor.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_holes.hfd"}, 4 | {"path": "designs/joints/finger_joint_plug.hfd"}, 5 | {"path": "designs/joints/finger_joint_socket.hfd"}, 6 | {"path": "designs/joints/line.hfd"} 7 | ], 8 | "params": { 9 | "offset": ".0035", 10 | "material_width": 20, 11 | "material_height": 12, 12 | "material_thickness": 0.2, 13 | "width": 5, 14 | "height": 6, 15 | "finger_width": 0.3, 16 | "finger_height": 0.2, 17 | "finger_space": 0.2, 18 | "finger_padding": 0.5, 19 | "finger_depth": "material_thickness" 20 | }, 21 | "parts": [ 22 | { 23 | "id" : "house_floor", 24 | "custom_component": { 25 | "type": "house_floor" 26 | }, 27 | "finger_padding": "(finger_space + finger_width) / 2", 28 | "transforms": [ 29 | { 30 | "type": "join", 31 | "close_path" : "true" 32 | }, 33 | { 34 | "type": "offset", 35 | "distance" : "offset", 36 | "size_should_be" : "larger" 37 | } 38 | ], 39 | "components": [ 40 | { 41 | "type": "finger_joint_socket", 42 | "from" : "0,0", 43 | "to" : "width,0" 44 | }, 45 | { 46 | "type": "finger_joint_socket", 47 | "from" : "width,0", 48 | "to" : "width,height" 49 | }, 50 | { 51 | "type": "finger_joint_socket", 52 | "from" : "width,height", 53 | "to" : "0,height" 54 | }, 55 | { 56 | "type": "finger_joint_socket", 57 | "from" : "0,height", 58 | "to" : "0,0" 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /designs/house/front.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_holes.hfd"}, 4 | {"path": "designs/joints/finger_joint_plug.hfd"}, 5 | {"path": "designs/joints/finger_joint_socket.hfd"}, 6 | {"path": "designs/joints/line.hfd"} 7 | ], 8 | "params": { 9 | "offset": ".0035", 10 | "material_width": 20, 11 | "material_height": 12, 12 | "material_thickness": 0.2, 13 | "width": 5, 14 | "height": 6, 15 | "finger_width": 0.3, 16 | "finger_height": 0.2, 17 | "finger_space": 0.2, 18 | "finger_padding": 0.5, 19 | "finger_depth": "material_thickness" 20 | }, 21 | "parts": [ 22 | { 23 | "id" : "house_front", 24 | "custom_component": { 25 | "type": "house_front" 26 | }, 27 | "finger_padding": "(finger_space + finger_width) / 2", 28 | "transforms": [ 29 | { 30 | "type": "join", 31 | "close_path" : "true" 32 | }, 33 | { 34 | "type": "offset", 35 | "distance" : "offset", 36 | "size_should_be" : "larger" 37 | } 38 | ], 39 | "components": [ 40 | { 41 | "type": "finger_joint_plug", 42 | "from" : "0,0", 43 | "to" : "width,0" 44 | }, 45 | { 46 | "type": "finger_joint_plug", 47 | "padding_left": "finger_padding", 48 | "padding_right": "finger_padding + material_thickness", 49 | "from": "width,0", 50 | "to" : "width,height" 51 | }, 52 | { 53 | "type": "line", 54 | "from" : "width,height", 55 | "to" : "0,height" 56 | }, 57 | { 58 | "type": "finger_joint_plug", 59 | "padding_right": "finger_padding", 60 | "padding_left": "finger_padding + material_thickness", 61 | "from": "0,height", 62 | "to" : "0,0" 63 | } 64 | ] 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /designs/house/side.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | {"path": "designs/joints/finger_joint_holes.hfd"}, 4 | {"path": "designs/joints/finger_joint_plug.hfd"}, 5 | {"path": "designs/joints/finger_joint_socket.hfd"}, 6 | {"path": "designs/joints/line.hfd"} 7 | ], 8 | "params": { 9 | "offset": ".0035", 10 | "material_width": 20, 11 | "material_height": 12, 12 | "material_thickness": 0.2, 13 | "width": 5, 14 | "height": 6, 15 | "roof_gable_height": 4, 16 | "finger_width": 0.3, 17 | "finger_height": 0.2, 18 | "finger_space": 0.2, 19 | "finger_padding": 0.5, 20 | "finger_depth": "material_thickness" 21 | }, 22 | "parts": [ 23 | { 24 | "id" : "house_side", 25 | "custom_component": { 26 | "type": "house_side_gabled" 27 | }, 28 | "finger_padding": "(finger_space + finger_width) / 2", 29 | "transforms": [ 30 | { 31 | "type": "join", 32 | "close_path" : "true" 33 | }, 34 | { 35 | "type": "offset", 36 | "distance" : "offset", 37 | "size_should_be" : "larger" 38 | } 39 | ], 40 | "components": [ 41 | { 42 | "type": "finger_joint_plug", 43 | "from" : "0,0", 44 | "to" : "width,0" 45 | }, 46 | { 47 | "type": "finger_joint_socket", 48 | "padding_left": "finger_padding", 49 | "padding_right": "finger_padding + material_thickness", 50 | "from": "width,0", 51 | "to" : "width,height" 52 | }, 53 | { 54 | "type": "finger_joint_plug", 55 | "handle": "$BOTTOM_LEFT", 56 | "from" : "width,height", 57 | "to" : "width / 2,height + roof_gable_height" 58 | }, 59 | { 60 | "type": "finger_joint_plug", 61 | "handle": "$BOTTOM_LEFT", 62 | "from" : "width / 2,height + roof_gable_height", 63 | "to" : "0,height" 64 | }, 65 | { 66 | "type": "finger_joint_socket", 67 | "padding_right": "finger_padding", 68 | "padding_left": "finger_padding + material_thickness", 69 | "from": "0,height", 70 | "to" : "0,0" 71 | } 72 | ] 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /designs/joints/dragon_joint_socket.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "imports": [ 3 | { 4 | "type": "svg", 5 | "path": "designs/svg/dragon.svg", 6 | "alias": "dragon" 7 | }, 8 | { 9 | "path": "designs/joints/dragon_joint_plug.hfd" 10 | } 11 | ], 12 | "params": { 13 | "dragon_width": 1, 14 | "dragon_space": 0.4 15 | }, 16 | "parts": [ 17 | { 18 | "id": "dragon_butt_joint", 19 | "components": [ 20 | { 21 | "type": "dragon_joint_plug", 22 | "custom_component": { 23 | "type": "dragon_joint_socket" 24 | }, 25 | "transforms": [ 26 | { 27 | "type": "mirror", 28 | "axis": "horizontal" 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /designs/joints/line.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "from": "0,0", 4 | "to": "1,1" 5 | }, 6 | "parts": [ 7 | { 8 | "components": [ 9 | { 10 | "type": "basic_edge", 11 | "custom_component": { 12 | "type": "line" 13 | }, 14 | "components" : [ 15 | { 16 | "type": "draw", 17 | "commands": [ 18 | { 19 | "command": "move", 20 | "to": "0,0" 21 | }, 22 | { 23 | "command": "line", 24 | "to": "width,0" 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | 31 | 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /designs/joints/slot_joint.hfd: -------------------------------------------------------------------------------- 1 | // Joint to join two parts in an X 2 | // TODO: unfinished 3 | { 4 | "params": { 5 | "from": "0,0", 6 | "to": "2,2", 7 | "material_thickness": 0.25, 8 | "slot_position": 0.5, 9 | "slot_depth": 0.5 10 | }, 11 | "parts": [ 12 | { 13 | "components": [ 14 | { 15 | "type": "draw", 16 | "custom_component": { 17 | "type": "slot_joint", 18 | "defaults": {} 19 | }, 20 | "start_point": "from", 21 | "end_point": "to", 22 | "transforms": [ 23 | { 24 | "type": "rotate", 25 | "degrees": "angle(start_point,end_point)" 26 | }, 27 | { 28 | "type": "move", 29 | "to": "start_point" 30 | } 31 | ], 32 | "commands": [ 33 | { 34 | "command": "move", 35 | "to": "0,0" 36 | }, 37 | { 38 | "command": "line", 39 | "to": "slot_position, 0" 40 | }, 41 | { 42 | "command": "line", 43 | "to": "slot_position, slot_depth" 44 | }, 45 | { 46 | "command": "line", 47 | "to": "slot_position + material_thickness, slot_depth" 48 | }, 49 | { 50 | "command": "line", 51 | "to": "slot_position + material_thickness, 0" 52 | }, 53 | { 54 | "command": "line", 55 | "to": { 56 | "x": "distance(start_point, end_point)", 57 | "y": "0" 58 | } 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /designs/joints/svg_edge.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "from": "0,0", 4 | "to": "1,1", 5 | "svg": "M 0,0 L 3,0" 6 | }, 7 | "parts": [ 8 | { 9 | "components": [ 10 | { 11 | "type": "basic_edge", 12 | "custom_component": { 13 | "type": "svg_edge" 14 | }, 15 | "components" : [ 16 | { 17 | "type": "draw", 18 | "commands": [ 19 | { 20 | "command": "move", 21 | "to": "0,0" 22 | }, 23 | { 24 | "command": "svg_connect_to", 25 | "to": "width,0", 26 | "svg": "svg" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /designs/joints/tab_joint_plug.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "from": "0,0", 4 | "to": "1,1", 5 | "tab_height": "0.2", 6 | "tab_width": ".5" 7 | }, 8 | "parts": [ 9 | { 10 | "components": [ 11 | { 12 | "type": "basic_edge", 13 | "custom_component": { 14 | "type": "tab_joint_plug" 15 | }, 16 | "components" : [ 17 | { 18 | "type": "draw", 19 | "commands": [ 20 | { 21 | "command": "move", 22 | "to": "0,tab_height" 23 | }, 24 | { 25 | "command": "rel_line", 26 | "to": "(width - tab_width) / 2,0" 27 | }, 28 | { 29 | "command": "rel_line", 30 | "to": "0, -tab_height" 31 | }, 32 | { 33 | "command": "rel_line", 34 | "to": "tab_width,0" 35 | }, 36 | { 37 | "command": "rel_line", 38 | "to": "0, tab_height" 39 | }, 40 | { 41 | "command": "rel_line", 42 | "to": "(width - tab_width) / 2,0" 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | 49 | 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /designs/joints/tab_joint_socket.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "from": "0,0", 4 | "to": "1,1", 5 | "tab_height": "0.2", 6 | "tab_width": ".5" 7 | }, 8 | "parts": [ 9 | { 10 | "components": [ 11 | { 12 | "type": "basic_edge", 13 | "custom_component": { 14 | "type": "tab_joint_plug" 15 | }, 16 | "components": [ 17 | { 18 | "type": "draw", 19 | "commands": [ 20 | { 21 | "command": "move", 22 | "to": "0,0" 23 | }, 24 | { 25 | "command": "rel_line", 26 | "to": "(width - tab_width) / 2,0" 27 | }, 28 | { 29 | "command": "rel_line", 30 | "to": "0, tab_height" 31 | }, 32 | { 33 | "command": "rel_line", 34 | "to": "tab_width,0" 35 | }, 36 | { 37 | "command": "rel_line", 38 | "to": "0, -tab_height" 39 | }, 40 | { 41 | "command": "rel_line", 42 | "to": "(width - tab_width) / 2,0" 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /designs/svg/candle_holder_gus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/svg/candle_holder_nadia.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/svg/dala_horse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/svg/lathe_pawn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/svg/line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /designs/svg/part_circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/svg/silverware_bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/svg/squiggle_line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/svg/test1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/svg/votive_1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designs/xmas_tree/spacer.hfd: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "width": 0.5, 4 | "stem_width": 0.25, 5 | "offset": 0.0035, 6 | "material_thickness": 0.2 7 | }, 8 | "parts": [ 9 | { 10 | "custom_component": { 11 | "type": "spacer" 12 | }, 13 | "components": [ 14 | { 15 | "type": "draw", 16 | "id": "spacer", 17 | "commands": [ 18 | { 19 | "command": "circle", 20 | "radius": "width / 2" 21 | } 22 | ] 23 | }, 24 | { 25 | "type": "draw", 26 | "id": "xmax_layer_hole", 27 | "transforms" : [ 28 | { 29 | "type": "offset", 30 | "distance" : "offset", 31 | "size_should_be" : "smaller" 32 | }, 33 | { 34 | "type": "move", 35 | "to": "width/2, width/2", 36 | "handle": "$MIDDLE_MIDDLE" 37 | } 38 | ], 39 | "commands": [ 40 | { 41 | "command": "rectangle", 42 | "width": "material_thickness", 43 | "height": "stem_width" 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /designs_rendered/around_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/button_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/button_layout_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/circle_with_center_rectangle_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/finger_joint_holes_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/finger_joint_plug_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/finger_joint_socket_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/front_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/front_flat_top_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/front_with_access_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/joystick_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/layer_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/lid_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /designs_rendered/line_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/nadia_display_box_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /designs_rendered/rectangle_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/rectangle_with_center_hole_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/roof_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/screw_holes_pair_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/screw_holes_rect_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/side_flat_top_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_001.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_002.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_003.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_004.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_005.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_006.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_007.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_008.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_009.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_010.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/silverware_011.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/slot_joint_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/spacer_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/speaker_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/stem_with_topper_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/svg_connect_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/svg_edge_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/tab_joint_plug_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/tab_joint_socket_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/votive_center_struts_001.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | votive_bottom:21 9 | 10 | 11 | -------------------------------------------------------------------------------- /designs_rendered/window_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /designs_rendered/xintercept_000.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = ../../heavyfishdesign-docs 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Documentation for HFD lives here. It is written in ReStructured Text and built by sphinx. 2 | 3 | To build: 4 | 5 | ``` 6 | $ cd docs && make html && cd .. 7 | ``` 8 | 9 | then open the local file in your browser 10 | 11 | ``` 12 | $ open ../heavyfishdesign-docs/html/index.html 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/around.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/around.png -------------------------------------------------------------------------------- /docs/source/_static/around_with_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/around_with_triangle.png -------------------------------------------------------------------------------- /docs/source/_static/box_with_access.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/box_with_access.jpg -------------------------------------------------------------------------------- /docs/source/_static/dragon_repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/dragon_repeat.png -------------------------------------------------------------------------------- /docs/source/_static/game_cab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/game_cab.jpg -------------------------------------------------------------------------------- /docs/source/_static/hfd_logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/hfd_logo1.png -------------------------------------------------------------------------------- /docs/source/_static/pawn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/pawn.jpg -------------------------------------------------------------------------------- /docs/source/_static/pawn_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/pawn_outline.png -------------------------------------------------------------------------------- /docs/source/_static/rotate_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/rotate_after.png -------------------------------------------------------------------------------- /docs/source/_static/rotate_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/rotate_before.png -------------------------------------------------------------------------------- /docs/source/_static/silverware.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/silverware.jpg -------------------------------------------------------------------------------- /docs/source/_static/slice_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/slice_after.png -------------------------------------------------------------------------------- /docs/source/_static/slice_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/slice_before.png -------------------------------------------------------------------------------- /docs/source/_static/square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/square.png -------------------------------------------------------------------------------- /docs/source/_static/xintercept_dala_horse_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/xintercept_dala_horse_1.png -------------------------------------------------------------------------------- /docs/source/_static/xintercept_dala_horse_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/xintercept_dala_horse_2.png -------------------------------------------------------------------------------- /docs/source/_static/xintercept_dala_horse_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/xintercept_dala_horse_3.png -------------------------------------------------------------------------------- /docs/source/_static/xmas_tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/_static/xmas_tree.jpg -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Heavy Fish Design' 21 | copyright = '2019, Dustin Norlander' 22 | author = 'Dustin Norlander' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | ] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # List of patterns, relative to source directory, that match files and 37 | # directories to ignore when looking for source files. 38 | # This pattern also affects html_static_path and html_extra_path. 39 | exclude_patterns = [] 40 | 41 | 42 | # -- Options for HTML output ------------------------------------------------- 43 | 44 | # The theme to use for HTML and HTML Help pages. See the documentation for 45 | # a list of builtin themes. 46 | # 47 | html_theme = 'alabaster' 48 | 49 | # Add any paths that contain custom static files (such as style sheets) here, 50 | # relative to this directory. They are copied after the builtin static files, 51 | # so a file named "default.css" will overwrite the builtin "default.css". 52 | html_static_path = ['_static'] 53 | 54 | html_sidebars = { 55 | '**': [ 56 | 'about.html', 57 | 'navigation.html', 58 | 'relations.html', 59 | 'donate.html', 60 | ] 61 | } 62 | 63 | 64 | html_theme_options = { 65 | 'logo': 'hfd_logo1.png', 66 | 'github_user': 'dustismo', 67 | 'github_repo': 'heavyfishdesign', 68 | 'show_powered_by' : False, 69 | } 70 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ========================== 3 | 4 | Contributions are welcome. See github.com/dustismo/hfd 5 | 6 | Components 7 | -------------------------- 8 | 9 | Components can be written by implementing the Component interface, creating a factory and 10 | registering the factory. 11 | 12 | 13 | Transforms 14 | -------------------------- 15 | 16 | Transforms simply do some operations on a Path and return a new path or an error. Transforms 17 | implement the PathTransforms interface, then register a TransformFactory to make it available 18 | -------------------------------------------------------------------------------- /docs/source/custom_components.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Custom Components 3 | ================= 4 | 5 | 6 | Creating Components via Markup 7 | ============================== 8 | 9 | It's easy to create custom components via the HFD markup language. Simply add a 10 | ``custom_component`` attribute to any component. The new component will be available 11 | via the ``import`` attribute on the document 12 | 13 | Example: 14 | 15 | Here is a simple line component: 16 | 17 | ``_ 18 | 19 | It requires the attributes ``to`` and ``from`` when being imported 20 | 21 | .. code-block:: JSON 22 | :linenos: 23 | :emphasize-lines: 7-13 24 | 25 | { 26 | "parts": [ 27 | { 28 | "components": [ 29 | { 30 | "type": "draw", 31 | "custom_component": { 32 | "type": "line" 33 | }, 34 | "commands": [ 35 | { 36 | "command": "move", 37 | "to": "from" 38 | }, 39 | { 40 | "command": "line", 41 | "to": "to" 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | 50 | 51 | Then to access the new component, simply import it and use it. 52 | 53 | .. code-block:: JSON 54 | :linenos: 55 | :emphasize-lines: 3, 16-20 56 | 57 | { 58 | "imports": [ 59 | {"path": "designs/common/line.hfd"} 60 | ], 61 | "params": { 62 | "material_width": 20, 63 | "material_height": 12, 64 | "material_thickness": 0.2, 65 | "width": 2, 66 | "height" 3 67 | }, 68 | "parts": [ 69 | { 70 | "id" : "line_example", 71 | "components": [ 72 | { 73 | "type": "line", 74 | "from" : "width,height", 75 | "to" : "width / 2,height / 2" 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /docs/source/designs.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustismo/heavyfishdesign/c49a731e111ba2c54989a4d385a00bd250a74c86/docs/source/designs.rst -------------------------------------------------------------------------------- /docs/source/expressions.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Expressions 3 | ================= 4 | 5 | HFD has a built in expression language that handles parameters, math and a handful of 6 | useful functions. 7 | 8 | ------------------------------------------------------------------------------------------ 9 | 10 | Available functionality in expressions 11 | ====================================== 12 | 13 | * ``+-*/()`` Normal arithmetic operators. 14 | * ``sqrt(arg)`` Square Root of arg 15 | * ``distance(p1,p2)`` OR ``distance(x1,y1,x2,y2)`` The distance between the points (x1,y1) and (x2,y2) 16 | * ``angle(p1,p2)`` OR ``angle(x1,y1,x2,y2)`` The angle in degrees of the line described by p1,p2 17 | * ``mmToInch(arg)`` Converts to inches from mm 18 | * ``inchToMM(arg)`` Converts to mm from inches 19 | 20 | Parameters 21 | ========== 22 | 23 | Parameters in any expression are looked up based on the current context. 24 | 25 | The order of lookup is as follows 26 | 27 | * Local attributes 28 | * Local params 29 | * Parent attributes 30 | * Parent params 31 | * ... Repeat until Document 32 | * Document params 33 | 34 | -------------------------------------------------------------------------------- /dom/common.go: -------------------------------------------------------------------------------- 1 | package dom 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/dustismo/heavyfishdesign/dynmap" 7 | ) 8 | 9 | type Units struct { 10 | Name string 11 | Abv string 12 | } 13 | 14 | func (u Units) FromInch(in float64) float64 { 15 | switch u.Abv { 16 | case "mm": 17 | return InchToMM(in) 18 | default: 19 | return in 20 | } 21 | } 22 | 23 | func (u Units) FromMM(mm float64) float64 { 24 | switch u.Abv { 25 | case "in": 26 | return MMToInch(mm) 27 | default: 28 | return mm 29 | } 30 | } 31 | 32 | var MilliMeters Units = Units{ 33 | "MilliMeters", "mm", 34 | } 35 | var Inches = Units{ 36 | "Inches", "in", 37 | } 38 | 39 | /* 40 | * Returns the requested units. and true. 41 | * Else returns MM, false 42 | */ 43 | func NewUnits(in string) (Units, bool) { 44 | switch strings.ToLower(in) { 45 | case "in", "inches": 46 | return Inches, true 47 | case "mm", "millimeters": 48 | return MilliMeters, true 49 | default: 50 | return MilliMeters, false 51 | } 52 | } 53 | 54 | func MustUnits(in string, defaultUnits Units) Units { 55 | u, ok := NewUnits(in) 56 | if !ok { 57 | return defaultUnits 58 | } 59 | return u 60 | } 61 | 62 | func MMToInch(mm float64) float64 { 63 | return mm / 25.4 64 | } 65 | 66 | func InchToMM(inch float64) float64 { 67 | return inch * 25.4 68 | } 69 | 70 | // Converts a list of components to the list of DynMaps 71 | func ComponentsToDynMap(c []Component) []*dynmap.DynMap { 72 | mps := []*dynmap.DynMap{} 73 | for _, cm := range c { 74 | mps = append(mps, cm.ToDynMap()) 75 | } 76 | return mps 77 | } 78 | -------------------------------------------------------------------------------- /dom/group_component.go: -------------------------------------------------------------------------------- 1 | package dom 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/dynmap" 5 | "github.com/dustismo/heavyfishdesign/path" 6 | "github.com/dustismo/heavyfishdesign/transforms" 7 | ) 8 | 9 | type GroupComponentFactory struct{} 10 | 11 | type GroupComponent struct { 12 | *BasicComponent 13 | components []Component 14 | segmentOperators path.SegmentOperators 15 | } 16 | 17 | func (ccf GroupComponentFactory) CreateComponent(componentType string, mp *dynmap.DynMap, dc *DocumentContext) (Component, error) { 18 | factory := AppContext() 19 | dm := mp.Clone() 20 | components, err := factory.MakeComponents(dm.MustDynMapSlice("components", []*dynmap.DynMap{}), dc) 21 | if err != nil { 22 | return nil, err 23 | } 24 | dm.Put("components", ComponentsToDynMap(components)) 25 | bc := factory.MakeBasicComponent(dm) 26 | bc.SetComponents(components) 27 | gc := &GroupComponent{ 28 | BasicComponent: bc, 29 | components: components, 30 | segmentOperators: factory.SegmentOperators(), 31 | } 32 | for _, c := range components { 33 | c.SetParent(gc) 34 | } 35 | return gc, nil 36 | } 37 | 38 | // The list of component types this Factory should be used for 39 | func (ccf GroupComponentFactory) ComponentTypes() []string { 40 | return []string{"group"} 41 | } 42 | 43 | func (cc *GroupComponent) Children() []Element { 44 | return CtoE(cc.components) 45 | } 46 | 47 | func (cc *GroupComponent) Render(ctx RenderContext) (path.Path, RenderContext, error) { 48 | cc.RenderStart(ctx) 49 | 50 | // render all the children then merge into one path. 51 | paths := []path.Path{} 52 | 53 | context := ctx.Clone() 54 | for _, e := range cc.components { 55 | p, c, err := e.Render(context) 56 | if err != nil { 57 | return p, c, err 58 | } 59 | paths = append(paths, p) 60 | context = c 61 | // find the new cursor location 62 | context.Cursor = path.PathCursor(p) 63 | } 64 | 65 | p := transforms.SimpleJoin{}.JoinPaths(paths...) 66 | return cc.HandleTransforms(cc, p, context) 67 | } 68 | -------------------------------------------------------------------------------- /dom/part_transform_factories.go: -------------------------------------------------------------------------------- 1 | package dom 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/dynmap" 5 | ) 6 | 7 | type PartSplitterTransformerFactory struct{} 8 | 9 | func (pf PartSplitterTransformerFactory) CreateTransformer(transformType string, dm *dynmap.DynMap, part *Part) (PartTransformer, error) { 10 | return &PartSplitter{mp: dm}, nil 11 | } 12 | 13 | // // The list of component types this Factory should be used for 14 | func (cf PartSplitterTransformerFactory) TransformerTypes() []string { 15 | return []string{"splitter"} 16 | } 17 | 18 | type PartLatheTransformerFactory struct{} 19 | 20 | func (pf PartLatheTransformerFactory) CreateTransformer(transformType string, dm *dynmap.DynMap, part *Part) (PartTransformer, error) { 21 | return &LathePartTransform{mp: dm}, nil 22 | } 23 | 24 | // // The list of component types this Factory should be used for 25 | func (cf PartLatheTransformerFactory) TransformerTypes() []string { 26 | return []string{"lathe"} 27 | } 28 | -------------------------------------------------------------------------------- /dom/render_context.go: -------------------------------------------------------------------------------- 1 | package dom 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/path" 5 | "github.com/dustismo/heavyfishdesign/util" 6 | ) 7 | 8 | type RenderContext struct { 9 | Origin path.Point 10 | Cursor path.Point 11 | Log *util.HfdLog 12 | } 13 | 14 | func (c RenderContext) Clone() RenderContext { 15 | return RenderContext{ 16 | Origin: c.Origin, 17 | Cursor: c.Cursor, 18 | Log: c.Log, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dynmap/sort_test.go: -------------------------------------------------------------------------------- 1 | package dynmap 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | //go test -v github.com/dustismo/open-books/dynmap 8 | func TestDynMapSort(t *testing.T) { 9 | arr := make([]DynMap, 5, 5) 10 | arr[0] = *New() 11 | arr[0].PutWithDot("test", "z") 12 | arr[1] = *New() 13 | arr[1].PutWithDot("test", "x") 14 | arr[2] = *New() 15 | arr[2].PutWithDot("test", "w") 16 | arr[3] = *New() 17 | arr[3].PutWithDot("test", "n") 18 | arr[4] = *New() 19 | arr[4].PutWithDot("test", "m") 20 | 21 | Sort(arr, "test") 22 | if arr[4].MustString("test", "") != "z" { 23 | t.Errorf("array sort failed. Expected z, got %s", arr[4].MustString("test", "")) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dustismo/heavyfishdesign 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/dustismo/govaluate v3.0.0+incompatible 7 | github.com/kr/text v0.2.0 // indirect 8 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 9 | github.com/sergi/go-diff v1.1.0 10 | github.com/stretchr/testify v1.6.1 // indirect 11 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 12 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/dustismo/heavyfishdesign/components" 7 | "github.com/dustismo/heavyfishdesign/path" 8 | "github.com/dustismo/heavyfishdesign/util" 9 | 10 | "github.com/dustismo/heavyfishdesign/dom" 11 | ) 12 | 13 | type DefaultDocumentParser struct { 14 | } 15 | 16 | func InitContext() { 17 | cf := []dom.ComponentFactory{ 18 | dom.PartFactory{}, 19 | dom.DrawComponentFactory{}, 20 | dom.RepeatComponentFactory{}, 21 | dom.GroupComponentFactory{}, 22 | components.EdgeComponentFactory{}, 23 | components.BasicEdgeComponentFactory{}, 24 | components.XInterceptComponentFactory{}, 25 | components.AroundComponentFactory{}, 26 | components.GearComponentFactory{}, 27 | } 28 | tf := []dom.TransformFactory{ 29 | dom.CleanupTransformFactory{}, 30 | dom.RotateTransformFactory{}, 31 | dom.ReverseTransformFactory{}, 32 | dom.JoinTransformFactory{}, 33 | dom.OffsetTransformFactory{}, 34 | dom.MoveTransformFactory{}, 35 | dom.TrimTransformFactory{}, 36 | dom.MatrixTransformFactory{}, 37 | dom.MirrorTransformFactory{}, 38 | dom.ScaleTransformFactory{}, 39 | dom.SliceTransformFactory{}, 40 | dom.RotateScaleTransformFactory{}, 41 | } 42 | 43 | pf := []dom.PartTransformerFactory{ 44 | dom.PartSplitterTransformerFactory{}, 45 | dom.PartLatheTransformerFactory{}, 46 | } 47 | docParser := NewDocumentParser() 48 | dom.AppContext().Init( 49 | cf, tf, pf, 50 | path.NewSegmentOperators(), 51 | docParser, 52 | docParser, 53 | SVGParser{}, 54 | ) 55 | } 56 | 57 | func NewDocumentParser() DefaultDocumentParser { 58 | return DefaultDocumentParser{} 59 | } 60 | 61 | func (p DefaultDocumentParser) Parse(bytes []byte, logger *util.HfdLog) (*dom.Document, error) { 62 | json := string(bytes) // convert content to a 'string' 63 | return dom.ParseDocumentFromJson(json, logger) 64 | } 65 | 66 | func (p DefaultDocumentParser) LoadBytes(filename string) ([]byte, error) { 67 | return ioutil.ReadFile(filename) 68 | } 69 | -------------------------------------------------------------------------------- /path/attributes_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestBoxMeasurements(t *testing.T) { 9 | so := NewSegmentOperators() 10 | p, err := ParsePathFromSvg("M 2 2 l 1 1 M 4 4") 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | point, err := PointPathAttribute(BottomRight, p, so) 16 | expected := "(X: 3.000, Y: 3.000)" 17 | actual := fmt.Sprintf("%s", point.StringPrecision(3)) 18 | if expected != actual { 19 | t.Errorf("Expected %s, but got %s\n", expected, actual) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /path/draw_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import "testing" 4 | 5 | func TestDrawCornerCurve(t *testing.T) { 6 | d := NewDraw() 7 | d.MoveTo(NewPoint(0, 0)) 8 | d.RoundedCornerTo(NewPoint(5, 5), NewPoint(0, 5), 2) 9 | p := d.Path() 10 | actual := SvgString(p, 3) 11 | expected := "M 0.000 0.000 L 0.000 3.000 C 0.000 4.105 0.895 5.000 2.000 5.000 L 5.000 5.000" 12 | if actual != expected { 13 | t.Errorf("Expected: %s\nActual:%s\n", expected, actual) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /path/path_parser_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import "testing" 4 | 5 | func TestPathParser(t *testing.T) { 6 | path := "m 6.1222251,228.86139 c 3.2732061,-2.13129 5.8707069,-4.49412 10.6737049,-3.83158 C 4.802998,0.66254 7.471412,20.84919 7.896028,31.75029" 7 | p, err := ParsePathFromSvg(path) 8 | 9 | if err != nil { 10 | t.Errorf("Error parsing path %s", err) 11 | } 12 | 13 | expectedStr := "M 6.122 228.861 C 9.395 226.730 11.993 224.367 16.796 225.030 C 4.803 0.663 7.471 20.849 7.896 31.750" 14 | actualStr := SvgString(p, 3) 15 | 16 | if expectedStr != actualStr { 17 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 18 | } 19 | } 20 | 21 | func TestPathParserWhitespace(t *testing.T) { 22 | path := " m 6,228 L 10 20" 23 | p, err := ParsePathFromSvg(path) 24 | 25 | if err != nil { 26 | t.Errorf("Error parsing path %s", err) 27 | } 28 | 29 | expectedStr := "M 6.000 228.000 L 10.000 20.000" 30 | actualStr := SvgString(p, 3) 31 | 32 | if expectedStr != actualStr { 33 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 34 | } 35 | } 36 | 37 | func TestPathParserDecimals(t *testing.T) { 38 | path := "L3.5,0" 39 | p, err := ParsePathFromSvg(path) 40 | 41 | if err != nil { 42 | t.Errorf("Error parsing path %s", err) 43 | } 44 | 45 | expectedStr := "L 3.500 0.000" 46 | actualStr := SvgString(p, 3) 47 | 48 | if expectedStr != actualStr { 49 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /path/path_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDistance(t *testing.T) { 8 | l := LineSegment{ 9 | StartPoint: NewPoint(0, 0), 10 | EndPoint: NewPoint(5, 5), 11 | } 12 | length := l.Length() 13 | expected := 7.071067811865475 14 | if !PrecisionEquals(length, expected, 3) { 15 | t.Errorf("Expected length %.3f but got %.3f\n", expected, length) 16 | } 17 | } 18 | 19 | func TestSlope(t *testing.T) { 20 | l := LineSegment{ 21 | StartPoint: NewPoint(0, 0), 22 | EndPoint: NewPoint(5, 5), 23 | } 24 | slope := l.Slope() 25 | if slope != 1 { 26 | t.Errorf("Expected slope %.3f but got %.3f\n", 1.0, slope) 27 | } 28 | } 29 | 30 | func TestLineAngle(t *testing.T) { 31 | l := LineSegment{ 32 | StartPoint: NewPoint(0, 0), 33 | EndPoint: NewPoint(3, 2), 34 | } 35 | angle := l.Angle() 36 | expected := 33.690 37 | if !PrecisionEquals(angle, expected, 3) { 38 | t.Errorf("Expected angle %.3f but got %.3f\n", expected, angle) 39 | } 40 | 41 | l = LineSegment{ 42 | StartPoint: NewPoint(3, 2), 43 | EndPoint: NewPoint(0, 0), 44 | } 45 | 46 | angle = l.Angle() 47 | expected = -146.310 48 | if !PrecisionEquals(angle, expected, 3) { 49 | t.Errorf("Expected angle %.3f but got %.3f\n", expected, angle) 50 | } 51 | } 52 | 53 | func TestLineByAngle(t *testing.T) { 54 | startPoint := NewPoint(0, 0) 55 | length := 2.0 56 | angle := 18.0 57 | l := NewLineSegmentAngle(startPoint, length, angle) 58 | if !PrecisionEquals(l.Angle(), angle, 3) { 59 | t.Errorf("Expected angle %.3f but got %.3f\n", angle, l.Angle()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /path/segment_operators_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestOffsetLine(t *testing.T) { 8 | ops := NewSegmentOperators() 9 | line := LineSegment{ 10 | StartPoint: NewPoint(10, 10), 11 | EndPoint: NewPoint(15, 30), 12 | } 13 | 14 | segs, err := ops.Offset(line, 5) 15 | if err != nil { 16 | t.Errorf("Error %v", err) 17 | } 18 | expected := "L 10.149 31.213" 19 | actual := segs[0].SvgString(3) 20 | 21 | if expected != actual { 22 | t.Errorf("Error: expected %s but got %s", expected, actual) 23 | } 24 | } 25 | 26 | // func TestSplit(t *testing.T) { 27 | // ops := NewSegmentOperators() 28 | // curve := CurveSegment{ 29 | // StartPoint: NewPoint(101.7, 202), 30 | // ControlPointStart: NewPoint(101, 202), 31 | // ControlPointEnd: NewPoint(200, 300), 32 | // EndPoint: NewPoint(300, 200), 33 | // } 34 | // // try to split on a point not on the line. 35 | // segs, err := ops.Split(curve, NewPoint(0, 15)) 36 | // if err != nil { 37 | // t.Errorf("Error %v", err) 38 | // } 39 | // path := NewPathFromSegments(segs) 40 | 41 | // expected := "" 42 | // actual := SvgString(path, 3) 43 | 44 | // if expected != actual { 45 | // t.Errorf("Error: expected %s\nbut got %s\n", expected, actual) 46 | // } 47 | // } 48 | 49 | func TestOffsetCurve(t *testing.T) { 50 | ops := NewSegmentOperators() 51 | curve := CurveSegment{ 52 | StartPoint: NewPoint(101.7, 202), 53 | ControlPointStart: NewPoint(101, 202), 54 | ControlPointEnd: NewPoint(200, 300), 55 | EndPoint: NewPoint(300, 200), 56 | } 57 | segs, err := ops.Offset(curve, 20) 58 | if err != nil { 59 | t.Errorf("Error %v", err) 60 | } 61 | path := NewPathFromSegments(segs) 62 | 63 | expected := "M 101.700 182.000 C 92.423 182.000 87.620 186.571 84.530 191.739 C 82.948 194.387 81.696 197.711 81.696 202.004 C 81.696 202.337 87.844 221.539 119.977 240.669 C 140.564 252.926 169.576 264.969 202.797 264.969 C 236.732 264.969 275.443 252.841 314.142 214.142" 64 | actual := SvgString(path, 3) 65 | 66 | if expected != actual { 67 | t.Errorf("Error: expected %s but got %s", expected, actual) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /path/transforms.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | // transform a Path into a new Path 4 | type PathTransform interface { 5 | PathTransform(path Path) (Path, error) 6 | } 7 | 8 | // transforma a segment 9 | type SegmentTransform interface { 10 | SegmentTransform(segment Segment) Segment 11 | } 12 | 13 | type Params interface { 14 | GetString(key string) string 15 | } 16 | 17 | // executes multiple transforms 18 | func MultiTransform(p Path, transforms ...PathTransform) (Path, error) { 19 | pth := p 20 | var err error 21 | for _, t := range transforms { 22 | pth, err = t.PathTransform(pth) 23 | if err != nil { 24 | return pth, err 25 | } 26 | } 27 | return pth, err 28 | } 29 | -------------------------------------------------------------------------------- /transforms/cleanup.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | type CleanupTransform struct { 10 | Precision int 11 | } 12 | 13 | // cleans up the path. 14 | // 1. make sure the first operation is a move 15 | // 2. Insert a Move if the segment Start is not the same as the segment end of the 16 | // previous 17 | // 3. align start and end if they are not equal, but within precision 18 | // 4. Check for NaN 19 | func (st CleanupTransform) PathTransform(p path.Path) (path.Path, error) { 20 | segments := []path.Segment{} 21 | var previousEnd path.Point 22 | for i, s := range p.Segments() { 23 | if i == 0 { 24 | if !path.IsMove(s) { 25 | segments = append(segments, path.MoveSegment{ 26 | EndPoint: s.Start(), 27 | }) 28 | } 29 | } else if !s.Start().Equals(previousEnd) { 30 | // add a Move segment.. 31 | if s.Start().EqualsPrecision(previousEnd, st.Precision) { 32 | // equals within the set precision. update the start point 33 | s, _ = path.SetSegmentStart(s, previousEnd) 34 | } 35 | segments = append(segments, path.MoveSegment{ 36 | StartPoint: previousEnd, 37 | EndPoint: s.Start(), 38 | }) 39 | } 40 | if !math.IsNaN(s.End().X) && !math.IsNaN(s.End().Y) { 41 | segments = append(segments, s) 42 | previousEnd = s.End() 43 | } 44 | } 45 | // make sure the first move starts at 0,0 46 | segments = path.FixHeadMove(segments) 47 | 48 | // now merge any moves 49 | segmentsTmp := []path.Segment{} 50 | for i, s := range segments { 51 | if i > 0 && path.IsMove(s) { 52 | lastInd := len(segmentsTmp) - 1 53 | // if the previous segment is a move, simply 54 | // overwrite it. 55 | prev := segmentsTmp[lastInd] 56 | if path.IsMove(prev) { 57 | segmentsTmp[lastInd] = path.MoveSegment{ 58 | StartPoint: prev.Start(), 59 | EndPoint: s.End(), 60 | } 61 | } else { 62 | segmentsTmp = append(segmentsTmp, s) 63 | } 64 | } else { 65 | segmentsTmp = append(segmentsTmp, s) 66 | } 67 | } 68 | segments = segmentsTmp 69 | 70 | return path.NewPathFromSegments(segments), nil 71 | } 72 | -------------------------------------------------------------------------------- /transforms/cleanup_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestCleanupBadStart(t *testing.T) { 10 | 11 | // test that the first move segment is from 0,0 not from 12 | // somewhere else 13 | segs := []path.Segment{ 14 | path.MoveSegment{ 15 | StartPoint: path.NewPoint(9, 3), 16 | EndPoint: path.NewPoint(0, 0), 17 | }, 18 | } 19 | 20 | p := path.NewPathFromSegmentsWithoutMove(segs) 21 | 22 | pth, err := CleanupTransform{Precision: 3}.PathTransform(p) 23 | 24 | if err != nil { 25 | t.Errorf("Error %s", err) 26 | } 27 | 28 | if pth.Segments()[0].Start().X != 0 || 29 | pth.Segments()[0].Start().Y != 0 { 30 | t.Errorf("Expected:0,0 start point") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /transforms/dedup.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/path" 5 | ) 6 | 7 | type DedupSegmentsTransform struct { 8 | Precision int 9 | } 10 | 11 | func (st DedupSegmentsTransform) doTransform(p path.Path) (path.Path, error) { 12 | segments := p.Segments() 13 | reverser := SegmentReverse{} 14 | if len(segments) < 2 { 15 | return p, nil 16 | } 17 | newSegments := make([]path.Segment, 0) 18 | newSegments = append(newSegments, segments[0]) 19 | 20 | for i := 1; i < len(segments); i++ { 21 | cur := segments[i] 22 | prev := newSegments[len(newSegments)-1] 23 | switch cur.(type) { 24 | case path.MoveSegment: 25 | // check if the prev was a move as well. 26 | // if so, we can overwrite the prev. 27 | if path.IsMove(prev) { 28 | prev = path.MoveSegment{ 29 | StartPoint: prev.Start(), 30 | EndPoint: cur.End(), 31 | } 32 | newSegments[len(newSegments)-1] = prev 33 | } else if prev.End().StringPrecision(st.Precision) == cur.End().StringPrecision(st.Precision) { 34 | // this moves the point to where it already is 35 | // we shouldn't add the move! 36 | } else { 37 | newSegments = append(newSegments, cur) 38 | } 39 | default: 40 | if cur.UniqueString(st.Precision) == reverser.SegmentTransform(prev).UniqueString(st.Precision) { 41 | // this pair is the same back and forwards. (like a line that writes over itself) 42 | if len(newSegments) > 0 { 43 | newSegments = newSegments[:len(newSegments)-1] 44 | } 45 | } else { 46 | newSegments = append(newSegments, cur) 47 | } 48 | } 49 | } 50 | return path.NewPathFromSegments(newSegments), nil 51 | } 52 | 53 | // this will remove any redundant operations. 54 | // currently: 55 | // 1. collapse multiple MOVEs in a row 56 | // 2. Remove a MOVE to the current location 57 | // 3. if prev segment is the same as the reverse of the current segment, remove both 58 | // TODO: should this be split into a its own transform? I wonder if this is ever the intent? 59 | func (st DedupSegmentsTransform) PathTransform(p path.Path) (path.Path, error) { 60 | pth, err := st.doTransform(p) 61 | if err != nil { 62 | return pth, err 63 | } 64 | // run the transform twice, since there are some 65 | // cases where the second pass is needed. 66 | // TODO: this is stupid, find a cleaner way. 67 | // return pth, err 68 | pth, err = st.doTransform(pth) 69 | if err != nil { 70 | return pth, err 71 | } 72 | return CleanupTransform{Precision: st.Precision}.PathTransform(pth) 73 | } 74 | -------------------------------------------------------------------------------- /transforms/matrix.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import "github.com/dustismo/heavyfishdesign/path" 4 | 5 | // A basic affine (matrix) transform 6 | // see: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#General_Transformation 7 | type MatrixTransform struct { 8 | A, B, C, D, E, F float64 9 | SegmentOperators path.SegmentOperators 10 | } 11 | 12 | func (mt MatrixTransform) TransformPoint(p path.Point) path.Point { 13 | xTransformed := p.X*mt.A + p.Y*mt.C + mt.E 14 | yTransformed := p.X*mt.B + p.Y*mt.D + mt.F 15 | return path.NewPoint(xTransformed, yTransformed) 16 | } 17 | 18 | func (mt MatrixTransform) PathTransform(p path.Path) (path.Path, error) { 19 | segments := []path.Segment{} 20 | 21 | for _, seg := range p.Segments() { 22 | s, err := mt.SegmentOperators.TransformPoints(seg, mt.TransformPoint) 23 | if err != nil { 24 | return nil, err 25 | } 26 | segments = append(segments, s) 27 | } 28 | 29 | return path.NewPathFromSegments(segments), nil 30 | } 31 | -------------------------------------------------------------------------------- /transforms/matrix_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestMatrixTransform(t *testing.T) { 10 | pathStr := "M 0,0 C 46.434 102.260 139.261 169.468 163.395 119.000 C 170.800 103.516 171.739 76.955 160.000 30.000" 11 | originalPath, err := path.ParsePathFromSvg(pathStr) 12 | 13 | if err != nil { 14 | t.Errorf("Error %s", err) 15 | } 16 | 17 | move := MatrixTransform{ 18 | A: 3, 19 | C: -1, 20 | E: 30, 21 | B: 1, 22 | D: 3, 23 | F: 40, 24 | SegmentOperators: path.NewSegmentOperators(), 25 | } 26 | 27 | p, err := move.PathTransform(originalPath) 28 | if err != nil { 29 | t.Errorf("Error %s", err) 30 | } 31 | expectedStr := "M 30.000 40.000 C 67.042 393.214 278.315 687.665 401.185 560.395 C 438.884 521.348 468.262 442.604 480.000 290.000" 32 | actualStr := path.SvgString(p, 3) 33 | 34 | if expectedStr != actualStr { 35 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /transforms/mirror.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/path" 5 | ) 6 | 7 | type Axis int 8 | 9 | const ( 10 | Horizontal Axis = iota 11 | Vertical 12 | ) 13 | 14 | // This moves the path origin to the requested point 15 | type MirrorTransform struct { 16 | Axis Axis 17 | Handle path.PathAttr 18 | SegmentOperators path.SegmentOperators 19 | } 20 | 21 | func (mt MirrorTransform) PathTransform(p path.Path) (path.Path, error) { 22 | segments := []path.Segment{} 23 | if len(mt.Handle) == 0 { 24 | // handle should be TOP_LEFT by default.. 25 | mt.Handle = path.TopLeft 26 | } 27 | axisPoint, err := path.PointPathAttribute(mt.Handle, p, mt.SegmentOperators) 28 | if err != nil { 29 | return p, err 30 | } 31 | // first move to 0,0 then mirror then move back. 32 | 33 | p, err = MoveTransform{ 34 | Point: path.NewPoint(0, 0), 35 | Handle: mt.Handle, 36 | SegmentOperators: mt.SegmentOperators, 37 | }.PathTransform(p) 38 | 39 | if err != nil { 40 | return p, err 41 | } 42 | _, br, err := path.BoundingBoxWithWhitespace(p, mt.SegmentOperators) 43 | if err != nil { 44 | return p, err 45 | } 46 | 47 | pt := func(p path.Point) path.Point { 48 | newPoint := p.Clone() 49 | if mt.Axis == Horizontal { 50 | // switch Y 51 | newPoint.Y = br.Y - p.Y 52 | } 53 | if mt.Axis == Vertical { 54 | // switch X 55 | newPoint.X = br.X - p.X 56 | } 57 | return newPoint 58 | } 59 | for _, seg := range p.Segments() { 60 | s, err := mt.SegmentOperators.TransformPoints(seg, pt) 61 | if err != nil { 62 | return nil, err 63 | } 64 | segments = append(segments, s) 65 | } 66 | 67 | pth := path.NewPathFromSegments(segments) 68 | 69 | // now move back to the original origin 70 | pth, err = ShiftTransform{ 71 | DeltaX: axisPoint.X, 72 | DeltaY: axisPoint.Y, 73 | SegmentOperators: mt.SegmentOperators, 74 | }.PathTransform(pth) 75 | 76 | return pth, err 77 | } 78 | -------------------------------------------------------------------------------- /transforms/mirror_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestMirrorTransform(t *testing.T) { 10 | pathStr := "M425.474,539.286L345.318,539.286C330.065,539.286 317.682,551.67 317.682,566.923L317.682,622.195C317.682,637.448 330.065,649.832 345.318,649.832C383.05,617.532 417.826,603.513 450.386,603.194C482.947,603.513 517.723,617.532 555.455,649.832C570.708,649.832 583.091,637.448 583.091,622.195L583.091,566.923C583.091,551.67 570.708,539.286 555.455,539.286L475.298,539.286" 11 | originalPath, err := path.ParsePathFromSvg(pathStr) 12 | 13 | if err != nil { 14 | t.Errorf("Error %s", err) 15 | } 16 | 17 | move := MirrorTransform{ 18 | Axis: Horizontal, 19 | Handle: path.MiddleMiddle, 20 | SegmentOperators: path.NewSegmentOperators(), 21 | } 22 | 23 | p, err := move.PathTransform(originalPath) 24 | if err != nil { 25 | t.Errorf("Error %s", err) 26 | } 27 | expectedStr := "M 425.474 705.105 L 345.318 705.105 C 330.065 705.105 317.682 692.721 317.682 677.468 L 317.682 622.196 C 317.682 606.943 330.065 594.559 345.318 594.559 C 383.050 626.859 417.826 640.878 450.386 641.197 C 482.947 640.878 517.723 626.859 555.455 594.559 C 570.708 594.559 583.091 606.943 583.091 622.196 L 583.091 677.468 C 583.091 692.721 570.708 705.105 555.455 705.105 L 475.298 705.105" 28 | actualStr := path.SvgString(p, 3) 29 | 30 | if expectedStr != actualStr { 31 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /transforms/move.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/path" 5 | ) 6 | 7 | // This moves the path origin to the requested point 8 | type MoveTransform struct { 9 | Point path.Point 10 | // if a handle is specified, than this move operation will move the 11 | // handle to the requested point. by default the handle is the topleft 12 | Handle path.PathAttr 13 | SegmentOperators path.SegmentOperators 14 | } 15 | 16 | func (mt MoveTransform) PathTransform(p path.Path) (path.Path, error) { 17 | if len(mt.Handle) == 0 { 18 | // handle should be TOP_LEFT by default.. 19 | mt.Handle = path.TopLeft 20 | } 21 | handle, err := path.PointPathAttribute(mt.Handle, p, mt.SegmentOperators) 22 | if err != nil { 23 | return p, err 24 | } 25 | mvX := mt.Point.X - handle.X 26 | mvY := mt.Point.Y - handle.Y 27 | 28 | return ShiftTransform{ 29 | DeltaX: mvX, 30 | DeltaY: mvY, 31 | SegmentOperators: mt.SegmentOperators, 32 | }.PathTransform(p) 33 | } 34 | -------------------------------------------------------------------------------- /transforms/move_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestMoveTransform(t *testing.T) { 10 | // https://codepen.io/anon/pen/orPwgd 11 | pathStr := "M 30.000 30.000 l 5,5 l 10,0" 12 | originalPath, err := path.ParsePathFromSvg(pathStr) 13 | 14 | if err != nil { 15 | t.Errorf("Error %s", err) 16 | } 17 | 18 | move := MoveTransform{ 19 | Point: path.NewPoint(10, 10), 20 | Handle: "$MIDDLE_MIDDLE", 21 | SegmentOperators: path.NewSegmentOperators(), 22 | } 23 | 24 | p, err := move.PathTransform(originalPath) 25 | if err != nil { 26 | t.Errorf("Error %s", err) 27 | } 28 | expectedStr := "M 2.500 7.500 L 7.500 12.500 L 17.500 12.500" 29 | actualStr := path.SvgString(p, 3) 30 | 31 | if expectedStr != actualStr { 32 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /transforms/path_reorder.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/path" 5 | ) 6 | 7 | type ReorderTransform struct { 8 | Precision int 9 | } 10 | 11 | // checks all the paths in pList to see which should be next. 12 | // Note this is a linear search, so this is fully N^2 13 | func (st ReorderTransform) doPath(p path.Path, pList []path.Path) (path.Path, []path.Path, error) { 14 | // based on p, this will return the next closest 15 | // path from pList 16 | _, endPoint := path.GetStartAndEnd(path.TrimTailMove(p.Segments())) 17 | 18 | // distances organized by index 19 | sDistances := make([]float64, len(pList)) 20 | eDistances := make([]float64, len(pList)) 21 | shortestSInd := 0 22 | shortestEInd := 0 23 | for i, pth := range pList { 24 | s, e := path.GetStartAndEnd(pth.Segments()) 25 | sDistances[i] = path.Distance(endPoint, s) 26 | eDistances[i] = path.Distance(endPoint, e) 27 | if sDistances[i] < sDistances[shortestSInd] { 28 | shortestSInd = i 29 | } 30 | 31 | if eDistances[i] < eDistances[shortestEInd] { 32 | shortestEInd = i 33 | } 34 | } 35 | 36 | // TODO: we should look at the end list as well 37 | // if eDistance is the shortest, we should 38 | // reverse it and return it. 39 | i := shortestSInd 40 | distance := sDistances[i] 41 | 42 | retPath := pList[i] 43 | if eDistances[shortestEInd] < sDistances[i] { 44 | // reverse and reset 45 | i = shortestEInd 46 | distance = eDistances[i] 47 | retPath = pList[i] 48 | r, err := PathReverse{}.PathTransform(retPath) 49 | if err != nil { 50 | return p, pList, err 51 | } 52 | retPath = r 53 | } 54 | 55 | // is the distance close enough? 56 | if !path.PrecisionEquals(distance, 0, st.Precision) { 57 | i = 0 58 | retPath = pList[0] 59 | } 60 | 61 | // remove the item from the list 62 | newList := append(pList[:i], pList[i+1:]...) 63 | return path.NewPathFromSegments(append(p.Segments(), retPath.Segments()...)), newList, nil 64 | } 65 | 66 | // Transform that will reorder any sections that 67 | // start and end at the same place 68 | func (st ReorderTransform) PathTransform(p path.Path) (path.Path, error) { 69 | paths := path.SplitPathOnMove(p) 70 | if len(paths) <= 1 { 71 | // nothing to do here! 72 | return p, nil 73 | } 74 | retList := []path.Segment{} 75 | retList = append(retList, paths[0].Segments()...) 76 | pList := paths[1:] 77 | pth := paths[0] 78 | for len(pList) > 0 { 79 | p1, pL, err := st.doPath(pth, pList) 80 | if err != nil { 81 | return p, err 82 | } 83 | 84 | pList = pL 85 | retList = append(retList, p1.Segments()...) 86 | pth = p1 87 | } 88 | return DedupSegmentsTransform{Precision: st.Precision}.PathTransform(pth) 89 | } 90 | -------------------------------------------------------------------------------- /transforms/path_reorder_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestReorder(t *testing.T) { 10 | pathStr := `M 0.000 0.000 L 5.000 0.000 11 | M 5.000 0.000 L 5.000 5.000 12 | M 0.000 5.000 L 5.000 5.000 13 | M 0.000 0.000 L 0.000 5.000` 14 | p, err := path.ParsePathFromSvg(pathStr) 15 | 16 | if err != nil { 17 | t.Errorf("Error %s", err) 18 | } 19 | reorder := ReorderTransform{ 20 | Precision: 3, 21 | } 22 | newPath, err := reorder.PathTransform(p) 23 | if err != nil { 24 | t.Errorf("Error %s", err) 25 | } 26 | // M 0.000 0.000 L 5.000 0.000 ____ 27 | // M 5.000 0.000 L 5.000 5.000 | right 28 | // M 0.000 5.000 L 5.000 5.000 ____ bottom 29 | // M 0.000 0.000 L 0.000 5.000 | 30 | 31 | expectedStr := `M 0.000 0.000 L 5.000 0.000 L 5.000 5.000 L 0.000 5.000 L 0.000 0.000` 32 | actualStr := path.SvgString(newPath, 3) 33 | 34 | if expectedStr != actualStr { 35 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 36 | } 37 | } 38 | 39 | func TestReorderComplex(t *testing.T) { 40 | pathStr := `M 0.000 -0.050 L 0.250 -0.050 M 0.300 0.000 L 0.300 0.500 M 0.250 0.450 L 1.000 0.450 M 0.950 0.500 L 0.950 0.000 M 1.000 -0.050 L 1.250 -0.050 M 1.250 -0.050 L 1.500 -0.050 M 1.550 0.000 L 1.550 0.500 M 1.500 0.450 L 2.250 0.450 M 2.200 0.500 L 2.200 0.000 M 2.250 -0.050 L 2.500 -0.050 M 2.500 -0.050 L 2.750 -0.050 M 2.800 0.000 L 2.800 0.500 M 2.750 0.450 L 3.500 0.450 M 3.450 0.500 L 3.450 0.000 M 3.500 -0.050 L 3.750 -0.050 M 3.750 -0.050 L 4.000 -0.050 M 4.050 0.000 L 4.050 0.500 M 4.000 0.450 L 4.750 0.450 M 4.700 0.500 L 4.700 0.000 M 4.750 -0.050 L 5.000 -0.050 M 5.050 0.000 L 5.050 5.000 M 5.000 5.050 L 0.000 5.050 M -0.050 5.000 L -0.050 0.000` 41 | p, err := path.ParsePathFromSvg(pathStr) 42 | 43 | if err != nil { 44 | t.Errorf("Error %s", err) 45 | } 46 | reorder := ReorderTransform{ 47 | Precision: 3, 48 | } 49 | newPath, err := reorder.PathTransform(p) 50 | if err != nil { 51 | t.Errorf("Error %s", err) 52 | } 53 | expectedStr := `M 0.000 -0.050 L 0.250 -0.050 M 0.300 0.000 L 0.300 0.500 M 0.250 0.450 L 1.000 0.450 M 0.950 0.500 L 0.950 0.000 M 1.000 -0.050 L 1.250 -0.050 L 1.500 -0.050 M 1.550 0.000 L 1.550 0.500 M 1.500 0.450 L 2.250 0.450 M 2.200 0.500 L 2.200 0.000 M 2.250 -0.050 L 2.500 -0.050 L 2.750 -0.050 M 2.800 0.000 L 2.800 0.500 M 2.750 0.450 L 3.500 0.450 M 3.450 0.500 L 3.450 0.000 M 3.500 -0.050 L 3.750 -0.050 L 4.000 -0.050 M 4.050 0.000 L 4.050 0.500 M 4.000 0.450 L 4.750 0.450 M 4.700 0.500 L 4.700 0.000 M 4.750 -0.050 L 5.000 -0.050 M 5.050 0.000 L 5.050 5.000 M 5.000 5.050 L 0.000 5.050 M -0.050 5.000 L -0.050 0.000` 54 | 55 | actualStr := path.SvgString(newPath, 3) 56 | 57 | if expectedStr != actualStr { 58 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /transforms/rebuild.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/path" 5 | ) 6 | 7 | type RebuildTransform struct{} 8 | 9 | // Simple transform to rebuild the path. 10 | // this dumps the path to a string and reparses 11 | // mostly this is for cleaning datastructure problems from other transforms 12 | // (I.E internal changes mean start and end points don't align) 13 | func (st RebuildTransform) PathTransform(p path.Path) (path.Path, error) { 14 | svgStr := path.SvgString(p, 7) 15 | return path.ParsePathFromSvg(svgStr) 16 | } 17 | -------------------------------------------------------------------------------- /transforms/reverse.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/path" 5 | ) 6 | 7 | type SegmentReverse struct { 8 | } 9 | 10 | type PathReverse struct { 11 | } 12 | 13 | // Reverses a single segment. Note that this flips the segment, but 14 | // will need a Move to start if you expect it to render properly within a path 15 | func (s SegmentReverse) SegmentTransform(segment path.Segment) path.Segment { 16 | // line to 17 | switch seg := segment.(type) { 18 | case path.MoveSegment: 19 | // easy, just switch the points 20 | return path.MoveSegment{ 21 | StartPoint: seg.End(), 22 | EndPoint: seg.Start(), 23 | } 24 | 25 | case path.LineSegment: 26 | // easy, just switch the points 27 | return path.LineSegment{ 28 | StartPoint: seg.End(), 29 | EndPoint: seg.Start(), 30 | } 31 | case path.CurveSegment: 32 | return path.CurveSegment{ 33 | StartPoint: seg.End(), 34 | EndPoint: seg.Start(), 35 | ControlPointStart: seg.ControlPointEnd, 36 | ControlPointEnd: seg.ControlPointStart, 37 | } 38 | } 39 | return segment 40 | } 41 | 42 | func (pr PathReverse) PathTransform(p path.Path) (path.Path, error) { 43 | originalSegments := p.Segments() 44 | // copy into a new array, so we don't mutate the original 45 | segments := make([]path.Segment, len(originalSegments)) 46 | copy(segments, originalSegments) 47 | reverser := SegmentReverse{} 48 | // reverse the segment order 49 | for i, j := 0, len(segments)-1; i < j; i, j = i+1, j-1 { 50 | segments[i], segments[j] = segments[j], segments[i] 51 | } 52 | 53 | // now convert back to a list of commands 54 | d := path.NewDraw() 55 | for _, seg := range segments { 56 | seg = reverser.SegmentTransform(seg) 57 | d.AddSegment(seg) 58 | } 59 | return d.Path(), nil 60 | } 61 | -------------------------------------------------------------------------------- /transforms/reverse_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestSegmentReverse(t *testing.T) { 10 | // https://codepen.io/anon/pen/orPwgd 11 | pathStr := "M 100.000 200.000 C 100.000 200.000 200.000 300.000 300.000 200.000" 12 | p, err := path.ParsePathFromSvg(pathStr) 13 | 14 | if err != nil { 15 | t.Errorf("Error %s", err) 16 | } 17 | segment := p.Segments()[1] 18 | reverse := SegmentReverse{} 19 | 20 | reversedSegment := reverse.SegmentTransform(segment) 21 | 22 | expectedStr := "C 200.000 300.000 100.000 200.000 100.000 200.000" 23 | actualStr := reversedSegment.SvgString(3) 24 | 25 | if expectedStr != actualStr { 26 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 27 | } 28 | } 29 | 30 | func TestPathReverse(t *testing.T) { 31 | // https://codepen.io/anon/pen/orPwgd 32 | pathStr := "M 101.7 202 C 101 202 200 300 300 200 L 312 222 L 321.9 201.98" 33 | p, err := path.ParsePathFromSvg(pathStr) 34 | 35 | if err != nil { 36 | t.Errorf("Error %s", err) 37 | } 38 | reversedPath, err := PathReverse{}.PathTransform(p) 39 | if err != nil { 40 | t.Errorf("Error %s", err) 41 | } 42 | reversedPath, _ = DedupSegmentsTransform{3}.PathTransform(reversedPath) 43 | expectedStr := "M 321.900 201.980 L 312.000 222.000 L 300.000 200.000 C 200.000 300.000 101.000 202.000 101.700 202.000 M 0.000 0.000" 44 | 45 | actualStr := path.SvgString(reversedPath, 3) 46 | 47 | if expectedStr != actualStr { 48 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /transforms/rotate.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "github.com/dustismo/heavyfishdesign/path" 5 | ) 6 | 7 | type RotateTransform struct { 8 | Degrees float64 9 | Axis path.PathAttr 10 | SegmentOperators path.SegmentOperators 11 | } 12 | 13 | func (rt RotateTransform) PathTransformWithAxis(pth path.Path, axisPoint path.Point) (path.Path, error) { 14 | 15 | segments := []path.Segment{} 16 | // first move to 0,0 then rotate then move back. 17 | 18 | p, err := MoveTransform{ 19 | Point: path.NewPoint(0, 0), 20 | Handle: rt.Axis, 21 | SegmentOperators: rt.SegmentOperators, 22 | }.PathTransform(pth) 23 | 24 | if err != nil { 25 | return p, err 26 | } 27 | pt := func(point path.Point) path.Point { 28 | return path.Rotate(rt.Degrees, point) 29 | } 30 | for _, seg := range p.Segments() { 31 | s, err := rt.SegmentOperators.TransformPoints(seg, pt) 32 | if err != nil { 33 | return nil, err 34 | } 35 | segments = append(segments, s) 36 | } 37 | 38 | pth, err = path.NewPathFromSegments(segments), nil 39 | if err != nil { 40 | return pth, err 41 | } 42 | 43 | // now move back to the original origin 44 | pth, err = ShiftTransform{ 45 | DeltaX: axisPoint.X, 46 | DeltaY: axisPoint.Y, 47 | SegmentOperators: rt.SegmentOperators, 48 | }.PathTransform(pth) 49 | 50 | return pth, err 51 | } 52 | 53 | func (rt RotateTransform) PathTransform(p path.Path) (path.Path, error) { 54 | if len(rt.Axis) == 0 { 55 | // handle should be TOP_LEFT by default.. 56 | rt.Axis = path.TopLeft 57 | } 58 | axisPoint, err := path.PointPathAttribute(rt.Axis, p, rt.SegmentOperators) 59 | if err != nil { 60 | return p, err 61 | } 62 | return rt.PathTransformWithAxis(p, axisPoint) 63 | } 64 | -------------------------------------------------------------------------------- /transforms/rotate_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestRotateTransform(t *testing.T) { 10 | // https://codepen.io/anon/pen/orPwgd 11 | pathStr := "M 50.000 50.000 l 20,0" 12 | originalPath, err := path.ParsePathFromSvg(pathStr) 13 | 14 | if err != nil { 15 | t.Errorf("Error %s", err) 16 | } 17 | 18 | transform := RotateTransform{ 19 | Degrees: 130, 20 | SegmentOperators: path.NewSegmentOperators(), 21 | } 22 | 23 | p, err := transform.PathTransform(originalPath) 24 | if err != nil { 25 | t.Errorf("Error %s", err) 26 | } 27 | expectedStr := "M 50.000 50.000 L 37.144 65.321" 28 | actualStr := path.SvgString(p, 3) 29 | 30 | if expectedStr != actualStr { 31 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /transforms/scale.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | // This changes the size and proportions of the given path 10 | type ScaleTransform struct { 11 | ScaleX float64 12 | ScaleY float64 13 | // TODO: Scaling by start and end point should 14 | // use the rotate_scale transform. 15 | StartPoint path.Point 16 | EndPoint path.Point 17 | Width float64 18 | Height float64 19 | SegmentOperators path.SegmentOperators 20 | } 21 | 22 | func (st ScaleTransform) PathTransform(p path.Path) (path.Path, error) { 23 | 24 | var xScale = st.ScaleX 25 | var yScale = st.ScaleY 26 | if st.Width > 0 || st.Height > 0 { 27 | // measure. 28 | tl, br, err := path.BoundingBoxTrimWhitespace(p, st.SegmentOperators) 29 | if err != nil { 30 | return p, err 31 | } 32 | curWidth := math.Abs(br.X - tl.X) 33 | curHeight := math.Abs(br.Y - tl.Y) 34 | if st.Width > 0 { 35 | xScale = st.Width / curWidth 36 | } 37 | if st.Height > 0 { 38 | yScale = st.Height / curHeight 39 | } else { 40 | yScale = xScale 41 | } 42 | if st.Width <= 0 { 43 | xScale = yScale 44 | } 45 | } else if !st.StartPoint.Equals(st.EndPoint) { 46 | // the start and end point are set, so we set the scale factors 47 | newX := math.Abs(st.EndPoint.X - st.StartPoint.X) 48 | newY := math.Abs(st.EndPoint.Y - st.StartPoint.Y) 49 | 50 | s, e := path.GetStartAndEnd(p.Segments()) 51 | oldX := math.Abs(e.X - s.X) 52 | oldY := math.Abs(e.Y - s.Y) 53 | 54 | xScale = newX / oldX 55 | yScale = newY / oldY 56 | 57 | // if we arent moving in one direction then set 58 | // equal scaling factors 59 | if newX == 0 || oldX == 0 { 60 | xScale = yScale 61 | } 62 | if newY == 0 || oldY == 0 { 63 | yScale = xScale 64 | } 65 | } 66 | 67 | segs := []path.Segment{} 68 | // function to do the scaling 69 | pt := func(p path.Point) path.Point { 70 | x := p.X * xScale 71 | y := p.Y * yScale 72 | return path.NewPoint(x, y) 73 | } 74 | 75 | for _, s := range p.Segments() { 76 | seg, err := st.SegmentOperators.TransformPoints(s, pt) 77 | if err != nil { 78 | return p, err 79 | } 80 | segs = append(segs, seg) 81 | } 82 | return path.NewPathFromSegments(segs), nil 83 | } 84 | -------------------------------------------------------------------------------- /transforms/scale_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestScaleTransform(t *testing.T) { 10 | pathStr := "M348.7,980.332L257.96,1029.19L329.328,881.936 C638.607,1066.56 477.01,980.332 477.01,980.332" 11 | originalPath, err := path.ParsePathFromSvg(pathStr) 12 | 13 | if err != nil { 14 | t.Errorf("Error %s", err) 15 | } 16 | 17 | transform := ScaleTransform{ 18 | StartPoint: path.NewPoint(0, 0), 19 | EndPoint: path.NewPoint(0.5, 0), 20 | SegmentOperators: path.NewSegmentOperators(), 21 | } 22 | 23 | p, err := transform.PathTransform(originalPath) 24 | if err != nil { 25 | t.Errorf("Error %s", err) 26 | } 27 | expectedStr := "M 1.359 3.820 L 1.005 4.011 L 1.283 3.437 C 2.489 4.156 1.859 3.820 1.859 3.820" 28 | actualStr := path.SvgString(p, 3) 29 | 30 | if expectedStr != actualStr { 31 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /transforms/shift.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import "github.com/dustismo/heavyfishdesign/path" 4 | 5 | // This shifts the path by the requested amount in X and/or Y 6 | type ShiftTransform struct { 7 | DeltaX float64 8 | DeltaY float64 9 | SegmentOperators path.SegmentOperators 10 | } 11 | 12 | func (st ShiftTransform) PathTransform(p path.Path) (path.Path, error) { 13 | pt := func(p path.Point) path.Point { 14 | return path.NewPoint(p.X+st.DeltaX, p.Y+st.DeltaY) 15 | } 16 | newPath := []path.Segment{} 17 | for _, seg := range p.Segments() { 18 | s, err := st.SegmentOperators.TransformPoints(seg, pt) 19 | if err != nil { 20 | return nil, err 21 | } 22 | newPath = append(newPath, s) 23 | } 24 | return path.NewPathFromSegments(newPath), nil 25 | } 26 | -------------------------------------------------------------------------------- /transforms/slice_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestSliceTransform(t *testing.T) { 10 | // https://codepen.io/dustismo/pen/xxxvgWm?editors=1001 11 | pathStr := "M 0,0 C 46.434 102.260 139.261 169.468 163.395 119.000 C 170.800 103.516 171.739 76.955 160.000 30.000" 12 | originalPath, err := path.ParsePathFromSvg(pathStr) 13 | 14 | if err != nil { 15 | t.Errorf("Error %s", err) 16 | } 17 | 18 | transform := HSliceTransform{ 19 | Y: 70, 20 | SegmentOperators: path.NewSegmentOperators(), 21 | Precision: 3, 22 | } 23 | 24 | p, err := transform.PathTransform(originalPath) 25 | if err != nil { 26 | t.Errorf("Error %s", err) 27 | } 28 | expectedStr := "M 0.000 0.000 C 11.837 26.067 26.688 49.857 42.647 70.000 M 167.684 70.000 C 166.344 58.647 163.856 45.424 160.000 30.000" 29 | actualStr := path.SvgString(p, 3) 30 | 31 | if expectedStr != actualStr { 32 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /transforms/trim_whitespace.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import "github.com/dustismo/heavyfishdesign/path" 4 | 5 | type TrimWhitespaceTransform struct { 6 | SegmentOperators path.SegmentOperators 7 | } 8 | 9 | // PathTransform trims any whitespace by moving the path to as close to 0,0 as possible. 10 | // Note that you should typically call simplify before triming whitespace to avoid 11 | // things like M 0 0, M 10, 11 12 | func (tw TrimWhitespaceTransform) PathTransform(p path.Path) (path.Path, error) { 13 | tl, _, err := path.BoundingBoxTrimWhitespace(p, tw.SegmentOperators) 14 | if err != nil { 15 | return p, err 16 | } 17 | 18 | return ShiftTransform{ 19 | DeltaX: -tl.X, 20 | DeltaY: -tl.Y, 21 | SegmentOperators: tw.SegmentOperators, 22 | }.PathTransform(p) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /transforms/trim_whitespace_test.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dustismo/heavyfishdesign/path" 7 | ) 8 | 9 | func TestTrimWhitespaceTransform(t *testing.T) { 10 | pathStr := `M 1.726 5.858 C 1.726 5.861 2.170 5.774 2.224 5.858 C 2.278 5.941 2.321 6.333 2.448 6.309 C 2.574 6.284 2.602 5.806 2.676 5.828 C 2.749 5.851 3.098 6.275 3.163 6.167 C 3.228 6.060 3.246 5.710 3.188 5.610 C 3.130 5.511 3.692 5.642 3.678 5.561 C 3.664 5.480 3.401 5.178 3.473 5.032 C 3.545 4.886 3.987 4.898 3.987 4.898 C 4.108 4.774 3.333 4.446 3.337 4.493 C 3.350 4.620 3.488 4.027 3.488 4.027 C 3.279 4.031 3.230 4.069 2.970 4.193 C 2.970 4.193 2.881 3.700 2.795 3.646 C 2.710 3.591 2.524 3.932 2.378 4.049 C 2.232 4.166 1.739 3.978 1.677 4.193 C 1.616 4.408 1.868 4.583 1.896 4.617 C 1.923 4.650 1.550 4.810 1.550 4.951 C 1.550 5.092 1.903 5.152 1.902 5.316 C 1.900 5.479 1.726 5.855 1.726 5.858 M 2.961 5.177 M 2.961 5.177 L 2.961 4.684 L 2.768 4.684 L 2.768 5.177 L 2.961 5.177` 11 | originalPath, err := path.ParsePathFromSvg(pathStr) 12 | 13 | if err != nil { 14 | t.Errorf("Error %s", err) 15 | } 16 | 17 | transform := TrimWhitespaceTransform{ 18 | SegmentOperators: path.NewSegmentOperators(), 19 | } 20 | 21 | p, err := transform.PathTransform(originalPath) 22 | if err != nil { 23 | t.Errorf("Error %s", err) 24 | } 25 | expectedStr := `M 0.176 2.218 C 0.176 2.221 0.620 2.134 0.674 2.218 C 0.728 2.301 0.771 2.693 0.898 2.669 C 1.024 2.644 1.052 2.166 1.126 2.188 C 1.199 2.211 1.548 2.635 1.613 2.527 C 1.678 2.420 1.696 2.070 1.638 1.970 C 1.580 1.871 2.142 2.002 2.128 1.921 C 2.114 1.840 1.851 1.538 1.923 1.392 C 1.995 1.246 2.437 1.258 2.437 1.258 C 2.558 1.134 1.783 0.806 1.787 0.853 C 1.800 0.980 1.938 0.387 1.938 0.387 C 1.729 0.391 1.680 0.429 1.420 0.553 C 1.420 0.553 1.331 0.060 1.245 0.006 C 1.160 -0.049 0.974 0.292 0.828 0.409 C 0.682 0.526 0.189 0.338 0.127 0.553 C 0.066 0.768 0.318 0.943 0.346 0.977 C 0.373 1.010 0.000 1.170 0.000 1.311 C 0.000 1.452 0.353 1.512 0.352 1.676 C 0.350 1.839 0.176 2.215 0.176 2.218 M 1.411 1.537 M 1.411 1.537 L 1.411 1.044 L 1.218 1.044 L 1.218 1.537 L 1.411 1.537` 26 | actualStr := path.SvgString(p, 3) 27 | 28 | if expectedStr != actualStr { 29 | t.Errorf("Expected: %s\nActual: %s", expectedStr, actualStr) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /util/file.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // deletes all files of the given type from the requested directory 10 | // this operates recursively, but does not remove the empty directories 11 | func ClearDir(directory, extension string) error { 12 | files, err := FileList(directory, extension) 13 | if err != nil { 14 | return err 15 | } 16 | for _, fn := range files { 17 | err = os.Remove(fn) 18 | if err != nil { 19 | return err 20 | } 21 | } 22 | return nil 23 | } 24 | 25 | // recursively search for files of the given extension 26 | 27 | // Filenames will have the passed in directory as a prefix: 28 | // for instance: 29 | // FileList("static", "hfd") 30 | // static/hfd/shelf/four_sided_shelf_with_divider.hfd 31 | // static/hfd/shelf/three_sided_shelf.hfd 32 | // static/hfd/shelf/three_sided_shelf_with_divider.hfd 33 | func FileList(directory string, extension string) ([]string, error) { 34 | filenames := []string{} 35 | err := filepath.Walk(directory, 36 | func(p string, info os.FileInfo, err error) error { 37 | if err != nil { 38 | return err 39 | } 40 | if !info.IsDir() && strings.HasSuffix(p, extension) { 41 | filenames = append(filenames, p) 42 | } 43 | return nil 44 | }) 45 | return filenames, err 46 | } 47 | 48 | // Same as FileList, but strips the requested directory prefix. 49 | // for instance: 50 | // FileList("static", "hfd") 51 | // hfd/shelf/four_sided_shelf_with_divider.hfd 52 | // hfd/shelf/three_sided_shelf.hfd 53 | // hfd/shelf/three_sided_shelf_with_divider.hfd 54 | func RelFileList(directory string, extension string) ([]string, error) { 55 | filenames := []string{} 56 | err := filepath.Walk(directory, 57 | func(p string, info os.FileInfo, err error) error { 58 | if err != nil { 59 | return err 60 | } 61 | if !info.IsDir() && strings.HasSuffix(p, extension) { 62 | p = strings.TrimPrefix(p, directory) 63 | p = strings.TrimPrefix(p, string(os.PathSeparator)) 64 | filenames = append(filenames, p) 65 | } 66 | return nil 67 | }) 68 | return filenames, err 69 | } 70 | 71 | // recursively list the directories 72 | func DirectoryList(directory string) ([]string, error) { 73 | directories := []string{} 74 | err := filepath.Walk(directory, 75 | func(p string, info os.FileInfo, err error) error { 76 | if err != nil { 77 | return err 78 | } 79 | if !info.IsDir() { 80 | directories = append(directories, p) 81 | } 82 | return nil 83 | }) 84 | return directories, err 85 | } 86 | --------------------------------------------------------------------------------