diff --git a/.DS_Store b/.DS_Store index 4dea40f..db3d046 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/python-tests.yaml b/.github/workflows/python-tests.yaml new file mode 100644 index 0000000..cbcfebe --- /dev/null +++ b/.github/workflows/python-tests.yaml @@ -0,0 +1,23 @@ +name: Python tests + +on: + push: + branches: [main, week-1, week-2, week-3] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install package and test tools + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[dev]" + + - name: Run unit tests + run: pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb1a4b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.py[cod] +*$py.class +venv/ +env/ +.venv/ +.DS_Store \ No newline at end of file diff --git a/anaconda_projects/db/project_filebrowser.db b/anaconda_projects/db/project_filebrowser.db new file mode 100644 index 0000000..ff23104 Binary files /dev/null and b/anaconda_projects/db/project_filebrowser.db differ diff --git a/app_generated_chart.png b/app_generated_chart.png new file mode 100644 index 0000000..69e8575 Binary files /dev/null and b/app_generated_chart.png differ diff --git a/docs/.ipynb_checkpoints/report-checkpoint.ipynb b/docs/.ipynb_checkpoints/report-checkpoint.ipynb new file mode 100644 index 0000000..9451011 --- /dev/null +++ b/docs/.ipynb_checkpoints/report-checkpoint.ipynb @@ -0,0 +1,97 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "73620923-8165-4d80-a81a-cb768294cc6f", + "metadata": {}, + "source": [ + "Week 3 Assignment: Knitting Pattern Calculator Project Report\n", + "\n", + "1. Project Overview\n", + "Knitting a custom sweater from the top down requires translating physical body measurements into a discrete grid of stitches and rows based on a individual knitting gauge. Manual conversion often leads to structural math errors. Furthermore, knitters wanting to incorporate multi-colored intarsia or stranded motifs (\"alpha patterns\") must manually map those pixel charts onto their specific garment dimensions, which is a slow and error-prone process.\n", + "\n", + "This project automates both tasks. It pairs a Streamlit web interface with a specialized Python backend that accepts pixel-based alpha patterns, calculates necessary fabric grading, and automatically generates a matching color chart, text instructions, and a visual preview of the finalized piece." + ] + }, + { + "cell_type": "markdown", + "id": "fea57056-3a79-4bde-a173-4e34421c522b", + "metadata": {}, + "source": [ + "2. Software Architecture\n", + "The software uses the standard Python src/ layout to isolate core business and graphical logic from the presentation layer.\n", + "knitting_pattern/\n", + "├── src/\n", + "│ ├── knitting_pattern/\n", + "│ │ ├── __init__.py # Exposes core calculation functions\n", + "│ │ ├── app.py # Streamlit web application interface\n", + "│ │ ├── math_engine.py # Math logic, matrix grid creation, and chart operations\n", + "│ │ └── image_engine.py # Visualization logic, plotting chart and recognizing alpha patterns\n", + "│ └── tests/\n", + "│ ├── test_image_engine.py\n", + "│ └── test_math_engine.py \n", + "Stitch and Row Allocation: The engine converts user dimensions (in centimeters) to absolute integers. \n", + "It uses ceiling rounding (math.ceil) since a knitter cannot execute a partial stitch. The underlying formula is:\n", + "stitches = [{width (cm)} * {stitch gauge} / 10)]\n", + "\n", + "Grid Canvas Representation: The backend models the fabric as a two-dimensional grid (nested lists or arrays). Unoccupied background fabric is initialized to 0.\n", + "Alpha Pattern Integration: The system reads pixel-based assets representing the alpha design. It maps these coordinates directly onto the sweater grid, reassigning target cells from background 0 to specific integer color IDs (e.g., 1, 2, 3). This acts as the layout for the color chart." + ] + }, + { + "cell_type": "markdown", + "id": "c6508f58-97f2-4e02-ad03-a1d7d445bfcf", + "metadata": {}, + "source": [ + "3. Current Implementation: Drop-Shoulder Top-Down Construction\n", + "The application currently models a traditional drop-shoulder construction, worked from the top down.\n", + "\n", + "Back Panel Cast-On: The program calculates the back shoulder width and establishes the upper grid boundary.\n", + "\n", + "Short-Row Contour Carving: To create an ergonomic shoulder slope and back-neck contour, the engine calculates short-row steps. It modifies the fabric matrix by marking unknitted coordinates as dead space (-1) while keeping active channels at 0. This allows the application to accurately trace variable-row knitting before mapping the alpha motif.\n", + "\n", + "Straight Body Extension: Once short rows are complete, the body grid expands downward in straight, uniform columns to form the classic drop-shoulder silhouette." + ] + }, + { + "cell_type": "markdown", + "id": "b8f45f76-aedd-47be-b76e-461a696b3812", + "metadata": {}, + "source": [ + "4. Current Limitations and Future Expansion\n", + "While the drop-shoulder math and base grid generation are operational, our development roadmap highlights three areas of expansion:\n", + "\n", + "High-Contrast Visualizations and Text Instructions\n", + "Problem: The current output shows a basic matrix visualization. It works for debugging but does not generate clear, printable charts or row-by-row text patterns for the user.\n", + "\n", + "Solution: Upgrade the visualization component with customized, high-contrast color maps to output downloadable PDF color charts. Concurrently, write a translation loop that reads the matrix rows and outputs automated text instructions (e.g., \"Row 12: K10 Turn; Row 13: P to end of row\").\n", + "\n", + "Geometric Silhouette Expansion\n", + "Problem: The system is limited to drop-shoulder shapes, which utilize straight armhole paths and simple rectangular panels.\n", + "\n", + "Solution: Expand the mathematical backend to calculate complex raglan lines and set-in sleeve caps. This requires developing dynamic decrease formulas to scale diagonal stitch lines across the grid canvas while maintaining the integrity of the centered alpha patterns." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:base] *", + "language": "python", + "name": "conda-base-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/report.ipynb b/docs/report.ipynb new file mode 100644 index 0000000..9451011 --- /dev/null +++ b/docs/report.ipynb @@ -0,0 +1,97 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "73620923-8165-4d80-a81a-cb768294cc6f", + "metadata": {}, + "source": [ + "Week 3 Assignment: Knitting Pattern Calculator Project Report\n", + "\n", + "1. Project Overview\n", + "Knitting a custom sweater from the top down requires translating physical body measurements into a discrete grid of stitches and rows based on a individual knitting gauge. Manual conversion often leads to structural math errors. Furthermore, knitters wanting to incorporate multi-colored intarsia or stranded motifs (\"alpha patterns\") must manually map those pixel charts onto their specific garment dimensions, which is a slow and error-prone process.\n", + "\n", + "This project automates both tasks. It pairs a Streamlit web interface with a specialized Python backend that accepts pixel-based alpha patterns, calculates necessary fabric grading, and automatically generates a matching color chart, text instructions, and a visual preview of the finalized piece." + ] + }, + { + "cell_type": "markdown", + "id": "fea57056-3a79-4bde-a173-4e34421c522b", + "metadata": {}, + "source": [ + "2. Software Architecture\n", + "The software uses the standard Python src/ layout to isolate core business and graphical logic from the presentation layer.\n", + "knitting_pattern/\n", + "├── src/\n", + "│ ├── knitting_pattern/\n", + "│ │ ├── __init__.py # Exposes core calculation functions\n", + "│ │ ├── app.py # Streamlit web application interface\n", + "│ │ ├── math_engine.py # Math logic, matrix grid creation, and chart operations\n", + "│ │ └── image_engine.py # Visualization logic, plotting chart and recognizing alpha patterns\n", + "│ └── tests/\n", + "│ ├── test_image_engine.py\n", + "│ └── test_math_engine.py \n", + "Stitch and Row Allocation: The engine converts user dimensions (in centimeters) to absolute integers. \n", + "It uses ceiling rounding (math.ceil) since a knitter cannot execute a partial stitch. The underlying formula is:\n", + "stitches = [{width (cm)} * {stitch gauge} / 10)]\n", + "\n", + "Grid Canvas Representation: The backend models the fabric as a two-dimensional grid (nested lists or arrays). Unoccupied background fabric is initialized to 0.\n", + "Alpha Pattern Integration: The system reads pixel-based assets representing the alpha design. It maps these coordinates directly onto the sweater grid, reassigning target cells from background 0 to specific integer color IDs (e.g., 1, 2, 3). This acts as the layout for the color chart." + ] + }, + { + "cell_type": "markdown", + "id": "c6508f58-97f2-4e02-ad03-a1d7d445bfcf", + "metadata": {}, + "source": [ + "3. Current Implementation: Drop-Shoulder Top-Down Construction\n", + "The application currently models a traditional drop-shoulder construction, worked from the top down.\n", + "\n", + "Back Panel Cast-On: The program calculates the back shoulder width and establishes the upper grid boundary.\n", + "\n", + "Short-Row Contour Carving: To create an ergonomic shoulder slope and back-neck contour, the engine calculates short-row steps. It modifies the fabric matrix by marking unknitted coordinates as dead space (-1) while keeping active channels at 0. This allows the application to accurately trace variable-row knitting before mapping the alpha motif.\n", + "\n", + "Straight Body Extension: Once short rows are complete, the body grid expands downward in straight, uniform columns to form the classic drop-shoulder silhouette." + ] + }, + { + "cell_type": "markdown", + "id": "b8f45f76-aedd-47be-b76e-461a696b3812", + "metadata": {}, + "source": [ + "4. Current Limitations and Future Expansion\n", + "While the drop-shoulder math and base grid generation are operational, our development roadmap highlights three areas of expansion:\n", + "\n", + "High-Contrast Visualizations and Text Instructions\n", + "Problem: The current output shows a basic matrix visualization. It works for debugging but does not generate clear, printable charts or row-by-row text patterns for the user.\n", + "\n", + "Solution: Upgrade the visualization component with customized, high-contrast color maps to output downloadable PDF color charts. Concurrently, write a translation loop that reads the matrix rows and outputs automated text instructions (e.g., \"Row 12: K10 Turn; Row 13: P to end of row\").\n", + "\n", + "Geometric Silhouette Expansion\n", + "Problem: The system is limited to drop-shoulder shapes, which utilize straight armhole paths and simple rectangular panels.\n", + "\n", + "Solution: Expand the mathematical backend to calculate complex raglan lines and set-in sleeve caps. This requires developing dynamic decrease formulas to scale diagonal stitch lines across the grid canvas while maintaining the integrity of the centered alpha patterns." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:base] *", + "language": "python", + "name": "conda-base-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 6cbe195..e09ae89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling >= 1.26"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["alpha_calc"] +packages = ["src/knitting_pattern"] [project] name = "knitting_pattern" @@ -14,12 +14,25 @@ authors = [ description = "Kitting pattern calculator tool for my programming course" readme = "README.md" requires-python = ">=3.9" +dependencies = [ + "numpy", + "Pillow", + "streamlit", + "matplotlib" +] classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -license = "MIT" -license-files = ["LICEN[CS]E*"] +license = {text = "MIT"} + +[project.optional-dependencies] +dev = [ + "pytest", +] [project.urls] Homepage = "https://github.com/Programming-The-Next-Step-2026/knitting_pattern.git" + +[tool.pytest.ini_options] +pythonpath = ["src"] \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store index 38e69b0..4fe34c6 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/Sweater_patter_colorchart_v1.0.png b/src/Sweater_patter_colorchart_v1.0.png new file mode 100644 index 0000000..67f0662 Binary files /dev/null and b/src/Sweater_patter_colorchart_v1.0.png differ diff --git a/src/alpha_calc/__init__.py b/src/alpha_calc/__init__.py deleted file mode 100644 index 71f61f5..0000000 --- a/src/alpha_calc/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Sat May 9 00:17:28 2026 - -@author: kasteivanauskaite -""" - -#code for alpha calculator/converter \ No newline at end of file diff --git a/src/alpha_calc/alpha_patterns.py b/src/alpha_calc/alpha_patterns.py deleted file mode 100644 index 3eed4fe..0000000 --- a/src/alpha_calc/alpha_patterns.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Sat May 9 00:18:15 2026 - -@author: kasteivanauskaite -""" - -#actual code for calc \ No newline at end of file diff --git a/src/dragon_pattern.jpg b/src/dragon_pattern.jpg new file mode 100644 index 0000000..d3cfb39 Binary files /dev/null and b/src/dragon_pattern.jpg differ diff --git a/src/knitting_pattern/__init__.py b/src/knitting_pattern/__init__.py new file mode 100644 index 0000000..35ec198 --- /dev/null +++ b/src/knitting_pattern/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sun May 17 20:22:07 2026 + +@author: kasteivanauskaite +""" + +from .example import add, calculate_mean + +# engine/__init__.py + +# This exposes these specific functions to the rest of your app +from .math_engine import ( + calculate_stitches, calculate_rows, calculate_garment_dimensions, + calculate_top_down_shoulder_shaping, generate_panel_grid, apply_top_down_mountains_to_grid, + calculate_back_neck_shaping, apply_back_short_rows_to_grid +) + +# Expose Image Engine Functions +from .image_engine import ( + get_hardcoded_heart, + scale_pattern_matrix_integer, + overlay_pattern_on_grid, + generate_chart_image +) \ No newline at end of file diff --git a/src/knitting_pattern/app.py b/src/knitting_pattern/app.py new file mode 100644 index 0000000..585a520 --- /dev/null +++ b/src/knitting_pattern/app.py @@ -0,0 +1,544 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sun May 17 23:04:52 2026 + +@author: kasteivanauskaite +run: streamlit run src/knitting_pattern/app.py +""" +import os +import streamlit as st +import numpy as np +from PIL import Image, ImageDraw + +# ========================================== +# INITIALIZE SESSION STATE (For Centering Buttons) +# ========================================== +if "x_pos" not in st.session_state: + st.session_state.x_pos = 20 +if "y_pos" not in st.session_state: + st.session_state.y_pos = 15 +if "scale" not in st.session_state: + st.session_state.scale = 2 + +# ========================================== +# IMPORTING YOUR BACKEND ENGINES +# ========================================== + +from knitting_pattern.math_engine import ( + calculate_stitches, calculate_rows, calculate_garment_dimensions, + calculate_top_down_shoulder_shaping, generate_panel_grid, + apply_top_down_mountains_to_grid, calculate_back_neck_shaping, + # (and any other functions you have) +) +from knitting_pattern.image_engine import ( + get_hardcoded_heart, scale_pattern_matrix_integer, overlay_pattern_on_grid, generate_chart_image +) + +# ========================================== +# HELPER: PROCESS UPLOADED IMAGES +# ========================================== + +def process_uploaded_image(pil_image, target_width, threshold_value): + """Converts an uploaded image into a binary matrix and a visual graph-paper preview.""" + + # 1. FIX TRANSPARENCY + if pil_image.mode in ('RGBA', 'LA') or (pil_image.mode == 'P' and 'transparency' in pil_image.info): + bg = Image.new('RGBA', pil_image.size, (255, 255, 255, 255)) + pil_image = Image.alpha_composite(bg, pil_image.convert('RGBA')) + + # 2. CALCULATE EXACT HEIGHT + aspect_ratio = pil_image.height / pil_image.width + target_height = int(target_width * aspect_ratio) + + # 3. BOX RESAMPLING + if hasattr(Image, 'Resampling'): + pil_image = pil_image.resize((target_width, target_height), Image.Resampling.BOX) + else: + pil_image = pil_image.resize((target_width, target_height), Image.BOX) + + gray = pil_image.convert('L') + arr = np.array(gray) + + # 4. THRESHOLD MATH + matrix = [[1 if val < threshold_value else 0 for val in row] for row in arr] + + # 5. BUILD THE HIGH-CONTRAST PREVIEW IMAGE + # 0 = Black (Pattern), 255 = White (Background) + preview_arr = np.array([[0 if val == 1 else 255 for val in row] for row in matrix], dtype=np.uint8) + preview_img = Image.fromarray(preview_arr) + + # Convert to RGB so we can draw colored/gray grid lines on it! + preview_img = preview_img.convert("RGB") + + # Scale it up by 10x + cell_size = 10 + preview_img = preview_img.resize((target_width * cell_size, target_height * cell_size), Image.NEAREST) + + # 6. DRAW THE GRAPH PAPER GRID + draw = ImageDraw.Draw(preview_img) + grid_color = (130, 130, 130) # Matching slate gray + + # Draw vertical grid lines + for x in range(0, preview_img.width, cell_size): + draw.line([(x, 0), (x, preview_img.height)], fill=grid_color, width=1) + # Draw horizontal grid lines + for y in range(0, preview_img.height, cell_size): + draw.line([(0, y), (preview_img.width, y)], fill=grid_color, width=1) + + # Draw a thick bounding box around the whole thing + draw.rectangle([(0, 0), (preview_img.width-1, preview_img.height-1)], outline=grid_color, width=2) + + return matrix, preview_img + +# ========================================== +# PAGE CONFIGURATION +# ========================================== +st.set_page_config(page_title="Knit & Pixel", page_icon="🧶", layout="wide") + +st.title("🧶 Knit & Pixel: Colorwork Generator") +st.markdown("Transform any alpha pattern into a perfectly scaled, knittable chart.") + +# ========================================== +# SIDEBAR: SETTINGS & MEASUREMENTS +# ========================================== +with st.sidebar: + st.header("1. Yarn & Gauge") + col1, col2 = st.columns(2) + with col1: + gauge_sts = st.number_input("Stitches / 10cm", min_value=5.0, max_value=40.0, value=18.0, step=0.5) + with col2: + gauge_rows = st.number_input("Rows / 10cm", min_value=5.0, max_value=50.0, value=24.0, step=0.5) + + st.divider() + + st.header("2. Sizing & Fit") + size_mode = st.radio("Sizing Method", ["Standard Sizing", "Advanced (Made-to-Measure)"]) + + if size_mode == "Standard Sizing": + target_size = st.selectbox("Select Size", ["XS", "S", "M", "L", "XL", "2XL"], index=2) + dimensions = calculate_garment_dimensions(target_size, "drop_shoulder") + chest_cm = dimensions["body_chest_circ"] + dimensions["ease_added"] + # Standard sizes usually have a graded length, we'll default to 60cm if not in your dictionary + garment_length_cm = dimensions.get("body_length_cm", 60.0) + st.info(f"Panel Width: {dimensions['panel_width_cm']} cm\nLength: {garment_length_cm} cm") + else: + target_size = "M" + st.markdown("**Enter your exact measurements:**") + chest_cm = st.number_input("Full Chest Circumference (cm)", min_value=50.0, max_value=160.0, value=96.0) + ease_cm = st.slider("Positive Ease (cm)", min_value=-5.0, max_value=30.0, value=15.0, help="Added to the chest circumference.") + chest_cm += ease_cm + + garment_length_cm = st.number_input("Desired Length (Hem to Shoulder in cm)", min_value=30.0, max_value=120.0, value=60.0, step=1.0) + + st.divider() + + st.header("3. Shaping & Pattern") + panel_type = st.radio("Sweater Panel", ["Front Panel", "Back Panel"], horizontal=True) + + if panel_type == "Front Panel": + use_shaping = st.toggle("Enable Front Shoulder Shaping", value=True) + else: + use_shaping = st.toggle("Enable Back German Short Rows", value=True) + + st.divider() + include_pattern = st.toggle("Include Alpha Pattern", value=True) + +# ========================================== +# CORE MATH: DYNAMIC GRID DIMENSIONS +# ========================================== +panel_width_cm = chest_cm / 2 +total_sts = calculate_stitches(panel_width_cm, gauge_sts) + +# This physically translates the length cm into exact matrix rows! +total_rows = calculate_rows(garment_length_cm, gauge_rows) + +sweater_grid = generate_panel_grid(total_sts, total_rows) + +if panel_type == "Front Panel": + shaping = calculate_top_down_shoulder_shaping(total_sts, target_size, gauge_rows) + if use_shaping: + sweater_grid = apply_top_down_mountains_to_grid(sweater_grid, shaping) + safe_y_start = shaping["mountain_rows"] + 1 + else: + safe_y_start = 0 + +elif panel_type == "Back Panel": + shaping = calculate_back_neck_shaping(total_sts, target_size, gauge_rows) + if use_shaping: + sweater_grid = apply_back_short_rows_to_grid(sweater_grid, shaping) + safe_y_start = shaping["mountain_rows"] + 1 + else: + safe_y_start = 0 + +# ========================================== +# MAIN PAGE: PATTERN SELECTION +# ========================================== +st.header("4. Select Alpha Pattern") + +tab1, tab2 = st.tabs(["Upload Image", "Use Preset Pattern"]) + +base_pattern_matrix = None + +with tab1: + uploaded_file = st.file_uploader("Upload a pixel graphic (.png, .jpg)", type=['png', 'jpg', 'jpeg']) + if uploaded_file is not None: + image = Image.open(uploaded_file) + + # 1. Sliders Side-by-Side (Making them smaller and neater) + st.markdown("**Image Settings:**") + col_w, col_t = st.columns(2) + with col_w: + target_width = st.slider("Target Width (Stitches)", min_value=5, max_value=total_sts, value=25, step=1) + with col_t: + threshold = st.slider("Darkness Threshold", min_value=1, max_value=255, value=128, step=1, + help="Slide to dial in the perfect outline.") + + # Pass the sliders into the function and get BOTH the matrix and the preview image back! + base_pattern_matrix, processed_preview = process_uploaded_image(image, target_width, threshold) + + # 2. Images Side-by-Side for Easy Comparison + st.markdown("**Comparison Preview:**") + col_img1, col_img2 = st.columns(2) + with col_img1: + st.image(image, caption="Original Image", use_container_width=True) + with col_img2: + st.image(processed_preview, caption="Knitting Matrix Vision", use_container_width=True) + +with tab2: + preset = st.selectbox("Choose a preset", ["8-Bit Heart"]) + if uploaded_file is None: + base_pattern_matrix = get_hardcoded_heart() + st.info("Using the default heart. Try uploading your own above!") + +# ========================================== +# NEW SECTION: REAL-TIME PLACEMENT +# ========================================== +st.divider() +st.header("5. Real-Time Placement") + +sweater_grid = generate_panel_grid(total_sts, total_rows) + +# THE NEW ROUTING LOGIC: Checks the sidebar toggle before doing the math! +if panel_type == "Front Panel": + shaping = calculate_top_down_shoulder_shaping(total_sts, target_size, gauge_rows, panel_type) + if use_shaping: + sweater_grid = apply_top_down_mountains_to_grid(sweater_grid, shaping) + safe_y_start = shaping["mountain_rows"] + 1 + else: + safe_y_start = 0 + +elif panel_type == "Back Panel": + shaping = calculate_back_neck_shaping(total_sts, target_size, gauge_rows) + if use_shaping: + sweater_grid = apply_back_short_rows_to_grid(sweater_grid, shaping) + # The back is a dome, so the top-center row is at 0! + safe_y_start = 0 + else: + safe_y_start = 0 + + +# Safety checks to prevent app crashes if the canvas resizes out from under the sliders! +if st.session_state.y_pos < safe_y_start: + st.session_state.y_pos = int(safe_y_start) +if st.session_state.x_pos > total_sts: + st.session_state.x_pos = total_sts // 2 + +col_controls, col_preview = st.columns([1, 1.5]) + +with col_controls: + st.markdown("Use the sliders to lock your alpha pattern to the grid.") + pattern_scale = st.slider("Scale Multiplier", min_value=1, max_value=10, key="scale") + + # Calculate physical width/height of the pattern right now + p_w = len(base_pattern_matrix[0]) * pattern_scale + p_h = len(base_pattern_matrix) * pattern_scale + + # ========================================== + # CALLBACK FUNCTIONS FOR BUTTONS + # These run *before* the UI renders! + # ========================================== + def center_x_callback(t_sts, p_width): + st.session_state.x_pos = max(0, (t_sts - p_width) // 2) + + def center_y_callback(t_rows, s_y_start, p_height): + space = t_rows - s_y_start + st.session_state.y_pos = int(s_y_start + (space - p_height) // 2) + + # X-Axis Controls + st.markdown("##### Horizontal Placement") + col_x_slider, col_x_btn = st.columns([3, 1]) + with col_x_slider: + st.slider("X Position", min_value=0, max_value=total_sts, key="x_pos", label_visibility="collapsed") + with col_x_btn: + # We attach the callback to the on_click parameter! + st.button("Center ↔", on_click=center_x_callback, args=(total_sts, p_w), use_container_width=True) + + # Y-Axis Controls + st.markdown("##### Vertical Placement") + col_y_slider, col_y_btn = st.columns([3, 1]) + with col_y_slider: + st.slider("Y Position", min_value=int(safe_y_start), max_value=total_rows, key="y_pos", label_visibility="collapsed") + with col_y_btn: + st.button("Center ↕", on_click=center_y_callback, args=(total_rows, safe_y_start, p_h), use_container_width=True) + +with col_preview: + scaled_pattern = scale_pattern_matrix_integer(base_pattern_matrix, pattern_scale) + + # 1. Base Canvas (White for the empty space around the sweater) + preview_canvas = np.full((total_rows, total_sts, 3), 255, dtype=np.uint8) + + # 2. Solid Sweater Base (No more checkerboard!) + for r in range(total_rows): + for c in range(total_sts): + if sweater_grid[r][c] != -1: + # Just a clean, solid light gray for the sweater panel + preview_canvas[r, c] = [190, 190, 190] + + # 3. Pattern Placement + if include_pattern and base_pattern_matrix is not None: + scaled_pattern = scale_pattern_matrix_integer(base_pattern_matrix, pattern_scale) + for r in range(len(scaled_pattern)): + for c in range(len(scaled_pattern[0])): + if scaled_pattern[r][c] == 1: + y = st.session_state.y_pos + r + x = st.session_state.x_pos + c + if 0 <= y < total_rows and 0 <= x < total_sts: + preview_canvas[y, x] = [220, 50, 50] + + preview_img = Image.fromarray(preview_canvas) + + # 4. Chunkier Upscaling + cell_size = 10 + preview_img = preview_img.resize((total_sts * cell_size, total_rows * cell_size), Image.NEAREST) + + # 5. Crisp Graph Paper Lines over the solid background + draw = ImageDraw.Draw(preview_img) + grid_color = (130, 130, 130) # Dark slate gray + + # Draw vertical grid lines + for x in range(0, preview_img.width, cell_size): + draw.line([(x, 0), (x, preview_img.height)], fill=grid_color, width=1) + # Draw horizontal grid lines + for y in range(0, preview_img.height, cell_size): + draw.line([(0, y), (preview_img.width, y)], fill=grid_color, width=1) + + # Draw a thick bounding box around the whole preview + draw.rectangle([(0, 0), (preview_img.width-1, preview_img.height-1)], outline=grid_color, width=2) + + st.image(preview_img, caption=f"Real-Time Placement ({total_sts} sts x {total_rows} rows)", use_container_width=True) + st.divider() + +# ========================================== +# THE MAGIC BUTTON (Final Export) +# ========================================== +st.markdown("### Happy with your placement?") +if st.button("✨ Generate Final Printable Chart", type="primary", use_container_width=True): + with st.spinner("Generating high-res Matplotlib chart..."): + + if include_pattern and base_pattern_matrix is not None: + scaled_pattern = scale_pattern_matrix_integer(base_pattern_matrix, pattern_scale) + final_chart = overlay_pattern_on_grid(sweater_grid, scaled_pattern, st.session_state.x_pos, st.session_state.y_pos) + else: + final_chart = sweater_grid + + image_filename = "app_generated_chart.png" + generate_chart_image(final_chart, shaping, image_filename) + + st.success("Chart generated successfully!") + +''' +import os +import streamlit as st +import numpy as np +from PIL import Image + +# ========================================== +# IMPORTING YOUR BACKEND ENGINES +# ========================================== +from knitting_pattern.math_engine import ( + calculate_stitches, calculate_rows, calculate_garment_dimensions, + calculate_top_down_shoulder_shaping, generate_panel_grid, apply_top_down_mountains_to_grid +) +from knitting_pattern.image_engine import ( + get_hardcoded_heart, scale_pattern_matrix_integer, overlay_pattern_on_grid, generate_chart_image +) + +# ========================================== +# HELPER: PROCESS UPLOADED IMAGES +# ========================================== +def process_uploaded_image(pil_image): + """Converts a user's uploaded image into a binary 1/0 matrix for knitting.""" + # Shrink it so massive photos don't crash the grid + pil_image.thumbnail((50, 50)) + # Convert to grayscale + gray = pil_image.convert('L') + arr = np.array(gray) + # Threshold: dark pixels become 1 (the pattern), light pixels become 0 (empty) + matrix = [[1 if val < 128 else 0 for val in row] for row in arr] + return matrix + +# ========================================== +# PAGE CONFIGURATION +# ========================================== +st.set_page_config(page_title="Knit & Pixel", page_icon="🧶", layout="wide") + +st.title("🧶 Knit & Pixel: Colorwork Generator") +st.markdown("Transform any alpha pattern into a perfectly scaled, knittable chart.") + +# ========================================== +# SIDEBAR: SETTINGS & MEASUREMENTS +# ========================================== +with st.sidebar: + st.header("1. Yarn & Gauge") + col1, col2 = st.columns(2) + with col1: + gauge_sts = st.number_input("Stitches / 10cm", min_value=5.0, max_value=40.0, value=18.0, step=0.5) + with col2: + gauge_rows = st.number_input("Rows / 10cm", min_value=5.0, max_value=50.0, value=24.0, step=0.5) + + st.divider() + + st.header("2. Sizing") + size_mode = st.radio("Sizing Method", ["Standard Sizing", "Advanced (Made-to-Measure)"]) + + if size_mode == "Standard Sizing": + target_size = st.selectbox("Select Size", ["XS", "S", "M", "L", "XL", "2XL"], index=2) + dimensions = calculate_garment_dimensions(target_size, "drop_shoulder") + chest_cm = dimensions["body_chest_circ"] + dimensions["ease_added"] + st.info(f"Panel Width: {dimensions['panel_width_cm']} cm") + else: + target_size = "M" + st.markdown("**Enter your exact body measurements:**") + chest_cm = st.number_input("Full Chest Circumference (cm)", min_value=50.0, max_value=160.0, value=96.0) + ease_cm = st.slider("Positive Ease (cm)", min_value=-5.0, max_value=30.0, value=15.0) + chest_cm += ease_cm + + st.divider() + + st.header("3. Shaping") + use_shoulder_shaping = st.toggle("Enable Shoulder Short Rows", value=True) + +# Calculate Core Dimensions early so the UI can use them! +panel_width_cm = chest_cm / 2 +total_sts = calculate_stitches(panel_width_cm, gauge_sts) +total_rows = 70 # The height of the preview canvas + +# ========================================== +# MAIN PAGE: PATTERN SELECTION +# ========================================== +st.header("4. Select Alpha Pattern") + +tab1, tab2 = st.tabs(["Upload Image", "Use Preset Pattern"]) + +base_pattern_matrix = None + +with tab1: + uploaded_file = st.file_uploader("Upload a pixel graphic (.png, .jpg)", type=['png', 'jpg', 'jpeg']) + if uploaded_file is not None: + image = Image.open(uploaded_file) + st.image(image, caption="Uploaded Graphic", width=150) + base_pattern_matrix = process_uploaded_image(image) + +with tab2: + preset = st.selectbox("Choose a preset", ["8-Bit Heart"]) + if uploaded_file is None: + base_pattern_matrix = get_hardcoded_heart() + st.info("Using the default heart. Try uploading your own above!") + +# ========================================== +# NEW SECTION: REAL-TIME PLACEMENT +# ========================================== +st.divider() +st.header("5. Real-Time Placement") + +# 1. CALCULATE SHAPING EARLY FOR THE PREVIEW +sweater_grid = generate_panel_grid(total_sts, total_rows) +shaping = calculate_top_down_shoulder_shaping(total_sts, target_size, gauge_rows) + +if use_shoulder_shaping: + sweater_grid = apply_top_down_mountains_to_grid(sweater_grid, shaping) + # The absolute highest row they are allowed to place a pattern + safe_y_start = shaping["mountain_rows"] + 1 +else: + safe_y_start = 0 + +col_controls, col_preview = st.columns([1, 1.5]) + +with col_controls: + st.markdown("Use the sliders to lock your alpha pattern to the grid.") + pattern_scale = st.slider("Scale Multiplier", min_value=1, max_value=10, value=2, step=1) + + start_x_stitch = st.slider("X Position (Left to Right)", min_value=0, max_value=total_sts, value=total_sts//2 - 5, step=1) + + # 2. LOCK THE Y SLIDER! + # min_value is now safe_y_start, preventing placement in the short rows! + start_y_row = st.slider("Y Position (Top to Bottom)", + min_value=int(safe_y_start), + max_value=total_rows, + value=int(safe_y_start) + 5, + step=1, + help="Pattern placement is locked below the neckline shaping.") + +with col_preview: + scaled_pattern = scale_pattern_matrix_integer(base_pattern_matrix, pattern_scale) + + # 3. BUILD CANVAS BASED ON ACTUAL SWEATER SHAPE + # Start with a white background (empty space) + preview_canvas = np.full((total_rows, total_sts, 3), 255, dtype=np.uint8) + + # Paint only the actual sweater stitches gray! + for r in range(total_rows): + for c in range(total_sts): + if sweater_grid[r][c] != -1: # If it's a real stitch + preview_canvas[r, c] = [235, 235, 235] if r % 2 == 0 else [245, 245, 245] + + # 4. Stamp the scaled pattern + p_h = len(scaled_pattern) + p_w = len(scaled_pattern[0]) + for r in range(p_h): + for c in range(p_w): + if scaled_pattern[r][c] == 1: + y = start_y_row + r + x = start_x_stitch + c + if 0 <= y < total_rows and 0 <= x < total_sts: + preview_canvas[y, x] = [255, 75, 75] + + preview_img = Image.fromarray(preview_canvas) + preview_img = preview_img.resize((total_sts * 8, total_rows * 8), Image.NEAREST) + + st.image(preview_img, caption=f"Real-Time Grid Preview", use_container_width=True) + +st.divider() + +# ========================================== +# THE MAGIC BUTTON (Final Export) +# ========================================== +st.markdown("### Happy with your placement?") +if st.button("✨ Generate Final Printable Chart", type="primary", use_container_width=True): + + with st.spinner("Generating high-res Matplotlib chart..."): + + # We already carved the sweater_grid above, so we just overlay! + final_chart = overlay_pattern_on_grid(sweater_grid, scaled_pattern, start_x_stitch, start_y_row) + + image_filename = "app_generated_chart.png" + generate_chart_image(final_chart, shaping, image_filename) + + st.success("Chart generated successfully!") + + col_results1, col_results2 = st.columns([1, 2]) + with col_results1: + st.subheader("Knitting Specs") + st.metric(label="Cast On Width", value=f"{total_sts} sts") + st.metric(label="Gauge (10cm)", value=f"{gauge_sts} sts x {gauge_rows} rows") + if use_shoulder_shaping: + st.metric(label="Short Row Steps", value=f"{shaping['total_short_row_steps']} steps") + + with col_results2: + st.subheader("Final Printable Chart") + if os.path.exists(image_filename): + st.image(image_filename, use_container_width=True) + else: + st.error("Failed to generate image file.") +''' diff --git a/src/knitting_pattern/example.py b/src/knitting_pattern/example.py new file mode 100644 index 0000000..208c723 --- /dev/null +++ b/src/knitting_pattern/example.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sun May 17 20:20:34 2026 + +@author: kasteivanauskaite +""" + +import numpy as np + +def add(x, y): + """ + Adds two numbers together. + + Args: + x: The first number. + y: The second number. + + Returns: + The sum of x and y. + + Example: + >>> add(2, 3) + 5 + """ + return x + y + +def calculate_mean(data): + """ + Calculates the average of a list of numbers using NumPy. + + Args: + data: A list or array of numbers. + + Returns: + The arithmetic mean as a float. + + Example: + >>> calculate_mean([1, 2, 3, 4, 5]) + 3.0 + """ + return float(np.mean(data)) \ No newline at end of file diff --git a/src/knitting_pattern/image_engine.py b/src/knitting_pattern/image_engine.py new file mode 100644 index 0000000..b0d6a9e --- /dev/null +++ b/src/knitting_pattern/image_engine.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sun May 17 21:38:58 2026 + +@author: kasteivanauskaite +""" + +# knitting_pattern/image_engine.py +import copy + +def get_hardcoded_heart(): + """ + Returns a small 2D matrix representing a heart. + 0 = Background yarn + 1 = Contrast color yarn + """ + return [ + [0, 1, 1, 0, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0] + ] +def scale_pattern_matrix_integer(original_matrix, multiplier): + """ + Scales a 2D array by an exact integer multiplier to preserve crisp pixel art. + A multiplier of 2 turns every 1x1 pixel into a 2x2 block of stitches. + """ + scaled_matrix = [] + + for row in original_matrix: + # Step 1: Duplicate horizontally (columns) + scaled_row = [] + for pixel in row: + scaled_row.extend([pixel] * int(multiplier)) + + # Step 2: Duplicate vertically (rows) + for _ in range(int(multiplier)): + # We append a copy() so we don't accidentally link the memory of the rows! + scaled_matrix.append(scaled_row.copy()) + + return scaled_matrix + +def overlay_pattern_on_grid(sweater_grid, alpha_matrix, start_x, start_y): + """ + Places the alpha pattern onto the main sweater grid. + Prevents the pattern from printing onto carved short-row areas (-1). + """ + # Create a copy so we don't accidentally ruin our blank canvas + result_grid = copy.deepcopy(sweater_grid) + + pattern_height = len(alpha_matrix) + pattern_width = len(alpha_matrix[0]) + + sweater_height = len(result_grid) + sweater_width = len(result_grid[0]) + + # Loop through every pixel in our little heart graphic + for p_y in range(pattern_height): + for p_x in range(pattern_width): + + # Calculate exactly where this pixel lands on the giant sweater grid + target_x = start_x + p_x + target_y = start_y + p_y + + # 1. BOUNDARY CHECK: Does it fall off the right or bottom edges? + if target_x < sweater_width and target_y < sweater_height: + + # 2. CARVING CHECK: Is this stitch a physical part of the sweater? + if result_grid[target_y][target_x] != -1: + + # 3. COLOR CHECK: Only print the contrast color (1), ignore the graphic's background (0) + if alpha_matrix[p_y][p_x] != 0: + result_grid[target_y][target_x] = alpha_matrix[p_y][p_x] + + return result_grid + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import ListedColormap + +def generate_chart_image(matrix, shaping_data, filename="knitting_chart.png"): + # 1. INJECT THE CAST-ON EDGE + # We change every physical stitch in Row 0 to a '2' so we can color it differently + for x in range(len(matrix[0])): + if matrix[0][x] != -1: # As long as it's a real stitch + matrix[0][x] = 2 + + # 2. SET UP THE COLORS + # -1: White (Empty Space) + # 0: Light Gray (Main Yarn) + # 1: Red (Heart/Contrast Yarn) + # 2: Gold (The Cast-On Swoop!) + data = np.array(matrix) + cmap = ListedColormap(['white', '#E0E0E0', '#FF4B4B', '#FFB000']) + + fig, ax = plt.subplots(figsize=(14, 10)) + cax = ax.imshow(data, cmap=cmap, vmin=-1, vmax=2) + + # 3. DRAW THE GRID + total_rows = len(matrix) + total_sts = len(matrix[0]) + + ax.set_xticks(np.arange(-.5, total_sts, 1), minor=True) + ax.set_yticks(np.arange(-.5, total_rows, 1), minor=True) + ax.grid(which="minor", color="black", linestyle='-', linewidth=0.5) + ax.tick_params(which="minor", size=0) + + # Hide the standard math axes + ax.set_xticks([]) + ax.set_yticks([]) + + # ========================================== + # 4. PROFESSIONAL RS/WS CHART NUMBERING + # ========================================== + + # We loop through every row. Odd rows get RS labels on the right. Even rows get WS labels on the left. + for y in range(total_rows): + # Only label every other row or specific intervals to avoid clutter, + # but let's do the first few specifically for the short rows! + if y == 0: + ax.text(total_sts, y, "CO (WS)", va='center', ha='left', fontsize=9, fontweight='bold', color='#FFB000') + elif y % 2 != 0: # Odd rows (Right Side) + ax.text(total_sts, y, f"Row {y} (RS)", va='center', ha='left', fontsize=8, color='black') + else: # Even rows (Wrong Side) + ax.text(-0.5, y, f"(WS) Row {y}", va='center', ha='right', fontsize=8, color='black') + + # ========================================== + # 5. ANNOTATING THE ASYMMETRIC TIMELINE + # ========================================== + mountain_rows = shaping_data["mountain_rows"] + total_sts = len(matrix[0]) + midpoint = total_sts // 2 + + # 1. REMOVED the heavy black hlines/vlines! + # Instead, we just draw a very subtle dashed line to show the physical boundary + # without making it look like a brick wall. + ax.hlines(y=mountain_rows + 0.5, xmin=midpoint, xmax=total_sts - 0.5, color='gray', linewidth=1, linestyle=':') + ax.hlines(y=mountain_rows + 1.5, xmin=-0.5, xmax=midpoint, color='gray', linewidth=1, linestyle=':') + + # 2. Add a VERTICAL ARROW to guide the eye straight up to Row 1 + # First, we draw just the arrow itself (no text attached) so it perfectly traces the midpoint + ax.annotate('', + xy=(midpoint, 1), xycoords='data', + xytext=(midpoint, mountain_rows - 0.5), textcoords='data', + arrowprops=dict(arrowstyle="->,head_length=0.8,head_width=0.4", + color="blue", lw=2.5, ls="--")) + + # Second, we place the text box slightly to the RIGHT of the arrow shaft (midpoint + 1.5) + ax.text(midpoint + 1.5, mountain_rows / 2, + 'Yarn continues\nto Row 1', + ha='left', va='center', color='blue', fontsize=10, fontweight='bold', + bbox=dict(facecolor='white', alpha=0.9, edgecolor='blue', boxstyle='round,pad=0.3')) + + # Point out the Right Shoulder + ax.text(total_sts - (shaping_data["first_turn_stitch"] / 2), mountain_rows / 2, + "Right Shoulder\n(Starts Row 1)", + ha="center", va="center", color="black", fontweight="bold", + bbox=dict(facecolor='white', alpha=0.8, edgecolor='black', boxstyle='round,pad=0.5')) + + # Point out the Left Shoulder + ax.text(shaping_data["first_turn_stitch"] / 2, (mountain_rows / 2) + 0.5, + "Left Shoulder\n(Starts Row 2)", + ha="center", va="center", color="black", fontweight="bold", + bbox=dict(facecolor='white', alpha=0.8, edgecolor='black', boxstyle='round,pad=0.5')) + + ax.set_title("Advanced Top-Down Panel: Asymmetrical Short Row Chart", pad=20, fontsize=16, fontweight='bold') + + plt.savefig(filename, dpi=300, bbox_inches='tight') + print(f"Professional chart successfully saved as {filename}!") + +#%% +# --- TESTING BLOCK --- +if __name__ == "__main__": + from knitting_pattern.math_engine import ( + calculate_stitches, calculate_rows, calculate_garment_dimensions, + calculate_top_down_shoulder_shaping, generate_panel_grid, apply_top_down_mountains_to_grid + ) + + my_gauge_sts = 18 + my_gauge_rows = 24 + + dimensions = calculate_garment_dimensions("M", "drop_shoulder") + total_sweater_sts = calculate_stitches(dimensions["panel_width_cm"], my_gauge_sts) + sweater_grid = generate_panel_grid(total_sweater_sts, 50) + + shaping = calculate_top_down_shoulder_shaping(total_sweater_sts, "M", my_gauge_rows) + carved_grid = apply_top_down_mountains_to_grid(sweater_grid, shaping) + + tiny_heart = get_hardcoded_heart() + heart_sts = calculate_stitches(15.0, my_gauge_sts) + heart_rows = calculate_rows(15.0, my_gauge_rows) + + giant_heart = scale_pattern_matrix(tiny_heart, heart_sts, heart_rows) + + start_x = (total_sweater_sts - heart_sts) // 2 + start_y = 12 + + final_chart = overlay_pattern_on_grid(carved_grid, giant_heart, start_x, start_y) + + # INSTEAD OF PRINTING TO TERMINAL, WE GENERATE THE IMAGE! + # Update this line at the bottom of your test block! + generate_chart_image(final_chart, shaping, "my_first_sweater_chart.png") + \ No newline at end of file diff --git a/src/knitting_pattern/math_engine.py b/src/knitting_pattern/math_engine.py new file mode 100644 index 0000000..8e661c4 --- /dev/null +++ b/src/knitting_pattern/math_engine.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sat May 9 00:18:15 2026 + +@author: kasteivanauskaite +""" +import math + +# ========================================== +# 1. GAUGE CALCULATORS +# ========================================== + +def calculate_stitches(width_cm, gauge_sts_per_10cm): + """ + Calculates the total number of stitches required for a specific width. + + Args: + width_cm (float): The desired width of the fabric in centimeters. + gauge_sts_per_10cm (float): The knitter's stitch gauge over 10 cm. + + Returns: + int: Total stitches needed, rounded up to the nearest whole integer. + """ + sts_per_cm = gauge_sts_per_10cm / 10.0 + return math.ceil(width_cm * sts_per_cm) + +def calculate_rows(length_cm, gauge_rows_per_10cm): + """ + Calculates the total number of rows required for a specific length. + + Args: + length_cm (float): The desired length of the fabric in centimeters. + gauge_rows_per_10cm (float): The knitter's row gauge over 10 cm. + + Returns: + int: Total rows needed, rounded up to the nearest whole integer. + """ + rows_per_cm = gauge_rows_per_10cm / 10.0 + return math.ceil(length_cm * rows_per_cm) + +# ========================================== +# 2. SIZING & EASE ENGINE +# ========================================== + +def get_standard_body_measurements(size_string): + """ + Retrieves standard human body circumferences based on sizing. + + Args: + size_string (str): The desired size (e.g., 'S', 'M', 'XL'). + + Returns: + dict: A dictionary containing 'chest_circ_cm', 'cross_back_cm', + and 'arm_length_cm' as floats. + """ + body_sizes = { + "XS": {"chest_circ_cm": 76.0, "cross_back_cm": 36.0, "arm_length_cm": 43.0}, + "S": {"chest_circ_cm": 86.0, "cross_back_cm": 38.0, "arm_length_cm": 44.0}, + "M": {"chest_circ_cm": 96.0, "cross_back_cm": 40.0, "arm_length_cm": 45.0}, + "L": {"chest_circ_cm": 106.0, "cross_back_cm": 42.0, "arm_length_cm": 46.0}, + "XL": {"chest_circ_cm": 117.0, "cross_back_cm": 44.0, "arm_length_cm": 47.0}, + "2XL": {"chest_circ_cm": 127.0, "cross_back_cm": 46.0, "arm_length_cm": 48.0} + } + return body_sizes.get(size_string.upper(), body_sizes["M"]) + +def get_style_ease(garment_type): + """ + Retrieves the standard ease allowance for a specific garment construction. + + Args: + garment_type (str): Type of garment (e.g., 'drop_shoulder', 'raglan'). + + Returns: + dict: Contains 'ease_cm' (float) to be added to the body circumference, + and a 'description' (str) of the fit. + """ + ease_dict = { + "drop_shoulder": {"ease_cm": 15.0, "description": "Loose, oversized fit"}, + "raglan": {"ease_cm": 7.5, "description": "Classic, comfortable fit"}, + "fitted_set_in": {"ease_cm": 2.5, "description": "Tailored, close to body"}, + "negative_ease_ribbed": {"ease_cm": -5.0, "description": "Stretches to fit tightly"} + } + return ease_dict.get(garment_type.lower(), ease_dict["drop_shoulder"]) + +def calculate_garment_dimensions(size_string, garment_type, custom_ease=None): + """ + Calculates final flat panel measurements by combining body size and ease. + + Args: + size_string (str): The desired body size (e.g., 'M'). + garment_type (str): The style of the garment. + custom_ease (float, optional): User-defined ease override in cm. Defaults to None. + + Returns: + dict: Final calculated measurements for knitting the panel. + """ + body = get_standard_body_measurements(size_string) + style = get_style_ease(garment_type) + + ease_to_add = custom_ease if custom_ease is not None else style["ease_cm"] + total_garment_circ = body["chest_circ_cm"] + ease_to_add + panel_width_cm = total_garment_circ / 2 + + return { + "body_chest_circ": body["chest_circ_cm"], + "ease_added": ease_to_add, + "total_garment_circ": total_garment_circ, + "panel_width_cm": panel_width_cm, + "arm_length_cm": body["arm_length_cm"], + "style_description": style["description"] + } + +# ========================================== +# 3. TOP-DOWN SHORT ROW & SHAPING ENGINE +# ========================================== + +def get_shaping_guidelines(size_string): + """ + Returns the graded centimeter drops for shoulder shaping. + Updated with deeper slopes for a highly tailored, less 'boxy' fit. + + Args: + size_string (str): The desired body size. + + Returns: + dict: Contains 'shoulder_drop_cm' and 'back_neck_raise_cm'. + """ + guidelines = { + "XS": {"shoulder_drop_cm": 3.5, "back_neck_raise_cm": 2.5}, + "S": {"shoulder_drop_cm": 4.0, "back_neck_raise_cm": 3.0}, + "M": {"shoulder_drop_cm": 4.5, "back_neck_raise_cm": 3.5}, + "L": {"shoulder_drop_cm": 5.5, "back_neck_raise_cm": 4.0}, + "XL": {"shoulder_drop_cm": 6.5, "back_neck_raise_cm": 4.5}, + "2XL": {"shoulder_drop_cm": 7.5, "back_neck_raise_cm": 5.0} + } + return guidelines.get(size_string.upper(), guidelines["M"]) + +def calculate_top_down_shoulder_shaping(total_chest_sts, size_string, gauge_rows_per_10cm, panel_type="Front Panel"): + """ + Calculates the short row intervals for top-down 'two mountain' shoulder shaping. + + Args: + total_chest_sts (int): The total cast-on width for the panel. + size_string (str): The body size, used to pull dynamic drop guidelines. + gauge_rows_per_10cm (float): The knitter's row gauge. + panel_type (str): "Front Panel" or "Back Panel". + """ + shaping_rules = get_shaping_guidelines(size_string) + + if panel_type == "Front Panel": + slope_drop_cm = shaping_rules["shoulder_drop_cm"] + else: + slope_drop_cm = shaping_rules["back_neck_raise_cm"] + + quarter_mark = total_chest_sts // 4 + middle_mark = total_chest_sts // 2 + + rows_per_cm = gauge_rows_per_10cm / 10.0 + mountain_rows = math.ceil(slope_drop_cm * rows_per_cm) + steps = max(1, mountain_rows // 2) + + distance_to_cover = middle_mark - quarter_mark + sts_per_step = distance_to_cover // steps + + return { + "mountain_rows": mountain_rows, + "total_short_row_steps": steps, + "first_turn_stitch": quarter_mark, + "middle_stitch": middle_mark, + "sts_to_knit_past_double_stitch": sts_per_step + } +def calculate_back_neck_shaping(total_chest_sts, size_string, gauge_rows_per_10cm): + shaping_rules = get_shaping_guidelines(size_string) + raise_cm = shaping_rules["back_neck_raise_cm"] + + rows_per_cm = gauge_rows_per_10cm / 10.0 + mountain_rows = math.ceil(raise_cm * rows_per_cm) + steps = max(1, mountain_rows // 2) + + middle_mark = total_chest_sts // 2 + neck_half_width = total_chest_sts // 6 + + shoulder_sts = (total_chest_sts // 2) - neck_half_width + sts_per_step = max(1, shoulder_sts // steps) + + return { + "mountain_rows": mountain_rows, + "total_short_row_steps": steps, + "middle_stitch": middle_mark, + "neck_half_width": neck_half_width, + "sts_per_step": sts_per_step + } + +def apply_back_short_rows_to_grid(grid, shaping_data): + steps = shaping_data["total_short_row_steps"] + middle = shaping_data["middle_stitch"] + neck_half_width = shaping_data["neck_half_width"] + sts_per_step = shaping_data["sts_per_step"] + total_sts = len(grid[0]) + + for step in range(steps): + live_left_edge = middle - neck_half_width - (step * sts_per_step) + live_right_edge = middle + neck_half_width + (step * sts_per_step) + + r1 = step * 2 + r2 = step * 2 + 1 + + if r1 < len(grid): + for col in range(0, live_left_edge): grid[r1][col] = -1 + for col in range(live_right_edge, total_sts): grid[r1][col] = -1 + + if r2 < len(grid): + for col in range(0, live_left_edge): grid[r2][col] = -1 + for col in range(live_right_edge, total_sts): grid[r2][col] = -1 + + return grid +# ========================================== +# 4. GRID GENERATION (THE CANVAS) +# ========================================== + +def generate_panel_grid(width_sts, length_rows): + """ + Creates a 2D matrix representing a blank, flat garment panel. + + Args: + width_sts (int): Total stitches (X-axis). + length_rows (int): Total rows (Y-axis). + + Returns: + list of lists: A 2D array filled with 0s. + """ + return [[0 for _ in range(width_sts)] for _ in range(length_rows)] + +def apply_top_down_mountains_to_grid(grid, shaping_data): + """ + Modifies the digital grid with ASYMMETRICAL top-down short row shaping. + The left shoulder shaping is delayed by 1 row compared to the right. + """ + steps = shaping_data["total_short_row_steps"] + quarter = shaping_data["first_turn_stitch"] + sts_per_step = shaping_data["sts_to_knit_past_double_stitch"] + + total_sts = len(grid[0]) + midpoint = total_sts // 2 + + # Row 0 is the full Cast-On sweep. Shaping starts on Row 1. + + for step in range(steps): + # RIGHT SHOULDER (Starts immediately on Row 1) + rs_row_1 = 1 + (step * 2) + rs_row_2 = 2 + (step * 2) + + # LEFT SHOULDER (Delayed by 1 row, starts Row 2) + ls_row_1 = 2 + (step * 2) + ls_row_2 = 3 + (step * 2) + + left_inner_edge = quarter + (step * sts_per_step) + right_inner_edge = total_sts - left_inner_edge + + # Carve RIGHT side of the neck (columns from midpoint to the right) + if rs_row_1 < len(grid): + for col in range(midpoint, right_inner_edge): + grid[rs_row_1][col] = -1 + if rs_row_2 < len(grid): + for col in range(midpoint, right_inner_edge): + grid[rs_row_2][col] = -1 + + # Carve LEFT side of the neck (columns from the left to midpoint) + if ls_row_1 < len(grid): + for col in range(left_inner_edge, midpoint): + grid[ls_row_1][col] = -1 + if ls_row_2 < len(grid): + for col in range(left_inner_edge, midpoint): + grid[ls_row_2][col] = -1 + + return grid + +# ========================================== +# TESTING BLOCK +# ========================================== +if __name__ == "__main__": + def print_grid_visually(grid, title="Grid"): + print(f"\n--- {title} ---") + for row in grid: + row_string = "".join(["██" if stitch == 0 else " " for stitch in row]) + print(row_string) + + print("Testing Math Engine with updated Top-Down logic...\n") + + # 40 stitches wide, 14 rows tall (just to see the top section) + test_grid = [[0 for _ in range(40)] for _ in range(14)] + + # Mock parameters for testing the new deeper slopes + test_shaping = { + "total_short_row_steps": 6, # Deeper slope requires more steps + "first_turn_stitch": 10, # 1/4 of 40 + "sts_to_knit_past_double_stitch": 1 # Slower progression towards center + } + + carved_grid = apply_top_down_mountains_to_grid(test_grid, test_shaping) + print_grid_visually(carved_grid, "Top-Down Mountains (Row 0 is Cast-On)") + + \ No newline at end of file diff --git a/src/mother_mary_pattern.jpg b/src/mother_mary_pattern.jpg new file mode 100644 index 0000000..5065d82 Binary files /dev/null and b/src/mother_mary_pattern.jpg differ diff --git a/src/pattern_chart_v2.0.png b/src/pattern_chart_v2.0.png new file mode 100644 index 0000000..501670d Binary files /dev/null and b/src/pattern_chart_v2.0.png differ diff --git a/src/pattern_chart_v2.1.png b/src/pattern_chart_v2.1.png new file mode 100644 index 0000000..44bdd42 Binary files /dev/null and b/src/pattern_chart_v2.1.png differ diff --git a/src/plain_grey_sweater_transp.png b/src/plain_grey_sweater_transp.png new file mode 100644 index 0000000..e5f32d0 Binary files /dev/null and b/src/plain_grey_sweater_transp.png differ diff --git a/src/rabbit_pattern.jpg b/src/rabbit_pattern.jpg new file mode 100644 index 0000000..b43fb27 Binary files /dev/null and b/src/rabbit_pattern.jpg differ diff --git a/src/star_pattern.jpg b/src/star_pattern.jpg new file mode 100644 index 0000000..9dffb8a Binary files /dev/null and b/src/star_pattern.jpg differ diff --git a/src/sun_pattern.jpg b/src/sun_pattern.jpg new file mode 100644 index 0000000..59acfa9 Binary files /dev/null and b/src/sun_pattern.jpg differ diff --git a/src/tests/test_example.py b/src/tests/test_example.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/test_image_engine.py b/src/tests/test_image_engine.py new file mode 100644 index 0000000..3c05028 --- /dev/null +++ b/src/tests/test_image_engine.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed May 20 15:30:57 2026 + +@author: kasteivanauskaite +""" +from knitting_pattern.math_engine import ( + generate_panel_grid, + apply_back_short_rows_to_grid +) + +# ========================================== +# 1. GRID GENERATION TESTS +# ========================================== +def test_generate_panel_grid(): + # Create a small 10 stitch wide by 5 row tall test grid + grid = generate_panel_grid(width_sts=10, length_rows=5) + + # Check that it generated exactly 5 rows (Y-axis) + assert len(grid) == 5 + # Check that the first row is exactly 10 stitches wide (X-axis) + assert len(grid[0]) == 10 + # Check that it is initialized with zeroes + assert grid[0][0] == 0 + + +# ========================================== +# 2. SHAPING MATRIX MANIPULATION TESTS +# ========================================== +def test_apply_back_short_rows_to_grid(): + # Create a clean 20x10 test grid + grid = generate_panel_grid(width_sts=20, length_rows=10) + + # We will pass in 'mock' shaping data so we know exactly what to expect + mock_shaping = { + "total_short_row_steps": 2, + "middle_stitch": 10, + "neck_half_width": 2, + "sts_per_step": 2 + } + + carved_grid = apply_back_short_rows_to_grid(grid, mock_shaping) + + # THE TEST: The outer corners should now be 'dead' canvas space (-1) + assert carved_grid[0][0] == -1 + assert carved_grid[0][-1] == -1 + + # THE TEST: The active center neck stitches should remain untouched (0) + assert carved_grid[0][10] == 0 \ No newline at end of file diff --git a/src/tests/test_math_engine.py b/src/tests/test_math_engine.py new file mode 100644 index 0000000..106a8cb --- /dev/null +++ b/src/tests/test_math_engine.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed May 20 15:14:10 2026 + +@author: kasteivanauskaite +""" +from knitting_pattern.math_engine import ( + calculate_stitches, + calculate_rows, + get_standard_body_measurements +) + +# ========================================== +# 1. GAUGE CALCULATOR TESTS +# ========================================== +def test_calculate_stitches(): + # If a fabric is 10cm wide, and gauge is 20 sts per 10cm, we expect 20 stitches. + assert calculate_stitches(10.0, 20.0) == 20 + + # If fabric is 15cm wide, and gauge is 22 sts per 10cm (2.2 sts/cm) + # 15 * 2.2 = 33 stitches + assert calculate_stitches(15.0, 22.0) == 33 + + # Testing rounding: 10.5cm * 2.2 = 23.1. Should round up to 24. + assert calculate_stitches(10.5, 22.0) == 24 + +def test_calculate_rows(): + # 10cm length at 30 rows/10cm should equal 30 rows + assert calculate_rows(10.0, 30.0) == 30 + +# ========================================== +# 2. SIZING DATA TESTS +# ========================================== +def test_get_standard_body_measurements(): + # Test that size Medium returns the correct dictionary + m_size = get_standard_body_measurements("M") + assert m_size["chest_circ_cm"] == 96.0 + + # Test that lowercase inputs are handled properly + s_size = get_standard_body_measurements("s") + assert s_size["chest_circ_cm"] == 86.0 + + # Test fallback: an invalid size should default to Medium (96.0) + invalid_size = get_standard_body_measurements("NONEXISTENT") + assert invalid_size["chest_circ_cm"] == 96.0 \ No newline at end of file