From 57b00ca3ec915895512ae224c9af5e8bca9fb3a5 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Mon, 20 Jan 2025 21:57:25 +0000 Subject: [PATCH 1/7] fix: Rendered username and year are low resolution causing poor prints. --- stl/geometry/text.go | 188 ++++++++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 84 deletions(-) diff --git a/stl/geometry/text.go b/stl/geometry/text.go index 65e83d0..0c8a8d9 100644 --- a/stl/geometry/text.go +++ b/stl/geometry/text.go @@ -19,15 +19,6 @@ type renderConfig struct { depth float64 } -// TextConfig holds parameters for text rendering -type textRenderConfig struct { - renderConfig - text string - contextWidth int - contextHeight int - fontSize float64 -} - // ImageConfig holds parameters for image rendering type imageRenderConfig struct { renderConfig @@ -36,29 +27,22 @@ type imageRenderConfig struct { } const ( - imagePosition = 0.025 - usernameOffset = -0.01 - yearPosition = 0.77 - - defaultContextWidth = 800 - defaultContextHeight = 200 - textVoxelSize = 1.0 - textDepthOffset = 2.0 - frontEmbedDepth = 1.5 - - usernameContextWidth = 1000 - usernameContextHeight = 200 - usernameFontSize = 48.0 - usernameZOffset = 0.7 - - yearContextWidth = 800 - yearContextHeight = 200 - yearFontSize = 56.0 - yearZOffset = 0.4 - - defaultImageHeight = 9.0 - defaultImageScale = 0.8 - imageLeftMargin = 10.0 + skylineFaceWidth = 142.5 // millimeters? + skylineFaceHeight = 10.0 // millimeters? + skylineResolutionWidth = 2000 // voxels + + logoPosition = 0.025 + logoHeight = 9.0 + logoScale = 0.8 + logoLeftMargin = 10.0 + + voxelDepth = 1.0 + + usernameFontSize = 120.0 + usernameLeftOffset = 0.1 + + yearFontSize = 100.0 + yearLeftOffset = 0.85 ) // Create3DText generates 3D text geometry for the username and year. @@ -67,40 +51,20 @@ func Create3DText(username string, year string, innerWidth, baseHeight float64) username = "anonymous" } - usernameConfig := textRenderConfig{ - renderConfig: renderConfig{ - startX: innerWidth * usernameOffset, - startY: -textDepthOffset / 2, - startZ: baseHeight * usernameZOffset, - voxelScale: textVoxelSize, - depth: frontEmbedDepth, - }, - text: username, - contextWidth: usernameContextWidth, - contextHeight: usernameContextHeight, - fontSize: usernameFontSize, - } - - yearConfig := textRenderConfig{ - renderConfig: renderConfig{ - startX: innerWidth * yearPosition, - startY: -textDepthOffset / 2, - startZ: baseHeight * yearZOffset, - voxelScale: textVoxelSize * 0.75, - depth: frontEmbedDepth, - }, - text: year, - contextWidth: yearContextWidth, - contextHeight: yearContextHeight, - fontSize: yearFontSize, - } - - usernameTriangles, err := renderText(usernameConfig) + usernameTriangles, err := renderText( + username, + usernameLeftOffset, + usernameFontSize, + ) if err != nil { return nil, err } - yearTriangles, err := renderText(yearConfig) + yearTriangles, err := renderText( + year, + yearLeftOffset, + yearFontSize, + ) if err != nil { return nil, err } @@ -108,9 +72,22 @@ func Create3DText(username string, year string, innerWidth, baseHeight float64) return append(usernameTriangles, yearTriangles...), nil } -// renderText generates 3D geometry for the given text configuration. -func renderText(config textRenderConfig) ([]types.Triangle, error) { - dc := gg.NewContext(config.contextWidth, config.contextHeight) +// renderText places text on the face of a skyline, offset from the left and vertically-aligned. +// The function takes the text to be displayed, offset from left, and font size. +// It returns an array of types.Triangle. +// +// Parameters: +// text (string): The text to be displayed on the skyline's front face. +// leftOffsetPercent (float64): The percentage distance from the left to start displaying the text. +// fontSize (float64): How large to make the text. Note: It scales with the skylineResolutionWidth. +// +// Returns: +// ([]types.Triangle, error): A slice of triangles representing text. +func renderText(text string, leftOffsetPercent float64, fontSize float64) ([]types.Triangle, error) { + // Create a rendering context for the face of the skyline + faceWidthRes := skylineResolutionWidth + faceHeightRes := int(float64(faceWidthRes) * skylineFaceHeight/skylineFaceWidth) + dc := gg.NewContext(faceWidthRes, faceHeightRes) // Get temporary font file fontPath, cleanup, err := writeTempFont(PrimaryFont) @@ -122,30 +99,33 @@ func renderText(config textRenderConfig) ([]types.Triangle, error) { } } - if err := dc.LoadFontFace(fontPath, config.fontSize); err != nil { + if err := dc.LoadFontFace(fontPath, fontSize); err != nil { return nil, errors.New(errors.IOError, "failed to load font", err) } dc.SetRGB(0, 0, 0) dc.Clear() dc.SetRGB(1, 1, 1) - dc.DrawStringAnchored(config.text, float64(config.contextWidth)/8, float64(config.contextHeight)/2, 0.0, 0.5) var triangles []types.Triangle - for y := 0; y < config.contextHeight; y++ { - for x := 0; x < config.contextWidth; x++ { + // Draw pixelated text in image at desired location + dc.DrawStringAnchored( + text, + float64(faceWidthRes)*leftOffsetPercent, // Offset from left + float64(faceHeightRes)*0.5, // Offset from top + 0.0, // Left aligned + 0.5, // Vertically aligned + ) + + // Transfer image pixels onto face of skyline as voxels + for x := 0; x < faceWidthRes; x++ { + for y := 0; y < faceHeightRes; y++ { if isPixelActive(dc, x, y) { - xPos := config.startX + float64(x)*config.voxelScale/8 - zPos := config.startZ - float64(y)*config.voxelScale/8 - - voxel, err := CreateCube( - xPos, - config.startY, - zPos, - config.voxelScale/2, - config.depth, - config.voxelScale/2, + voxel, err := createVoxelOnFace( + float64(x), + float64(y), + voxelDepth, ) if err != nil { return nil, errors.New(errors.STLError, "failed to create cube", err) @@ -161,6 +141,46 @@ func renderText(config textRenderConfig) ([]types.Triangle, error) { return triangles, nil } +// createVoxelOnFace creates a voxel on the face of a skyline by generating a cube at the specified coordinates. +// The function takes in the x, y coordinates and height. +// It returns a slice of types.Triangle representing the cube and an error if the cube creation fails. +// +// Parameters: +// x (float64): The x-coordinate on the skyline face (left to right). +// y (float64): The y-coordinate on the skyline face (top to bottom). +// height (float64): Distance coming out of the face. +// +// Returns: +// ([]types.Triangle, error): A slice of triangles representing the cube and an error if any. +func createVoxelOnFace(x float64, y float64, height float64) ([]types.Triangle, error) { + // Mapping resolution + xResolution := float64(skylineResolutionWidth) + yResolution := xResolution * skylineFaceHeight / skylineFaceWidth + + // Pixel size + voxelSize := 1.0; + + // Scale coordinate to face resolution + x = (x / xResolution) * skylineFaceWidth + y = (y / yResolution) * skylineFaceHeight + voxelSizeX := (voxelSize / xResolution) * skylineFaceWidth; + voxelSizeY := (voxelSize / yResolution) * skylineFaceHeight; + + cube, err := CreateCube( + // Location (from top left corner of skyline face) + x, // x - Left to right + -height, // y - Negative comes out of face. Positive goes into face. + -voxelSizeY - y, // z - Bottom to top + + // Size + voxelSizeX, // x length - left to right from specified point + height, // thickness - distance coming out of face + voxelSizeY, // y length - bottom to top from specified point + ) + + return cube, err +} + // GenerateImageGeometry creates 3D geometry from the embedded logo image. func GenerateImageGeometry(innerWidth, baseHeight float64) ([]types.Triangle, error) { // Get temporary image file @@ -171,14 +191,14 @@ func GenerateImageGeometry(innerWidth, baseHeight float64) ([]types.Triangle, er config := imageRenderConfig{ renderConfig: renderConfig{ - startX: innerWidth * imagePosition, - startY: -frontEmbedDepth / 2.0, + startX: innerWidth * logoPosition, + startY: -voxelDepth / 2.0, startZ: -0.85 * baseHeight, - voxelScale: defaultImageScale, - depth: frontEmbedDepth, + voxelScale: logoScale, + depth: voxelDepth, }, imagePath: imgPath, - height: defaultImageHeight, + height: logoHeight, } defer cleanup() From af2ed2d5c13ad157d8387ff15fda75f4cbae4aff Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Tue, 21 Jan 2025 14:40:29 +0000 Subject: [PATCH 2/7] refactor: Simplify code for displaying logo. --- stl/geometry/text.go | 75 +++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/stl/geometry/text.go b/stl/geometry/text.go index 0c8a8d9..3685e8c 100644 --- a/stl/geometry/text.go +++ b/stl/geometry/text.go @@ -30,19 +30,18 @@ const ( skylineFaceWidth = 142.5 // millimeters? skylineFaceHeight = 10.0 // millimeters? skylineResolutionWidth = 2000 // voxels - - logoPosition = 0.025 - logoHeight = 9.0 - logoScale = 0.8 - logoLeftMargin = 10.0 voxelDepth = 1.0 + + logoScale = 0.4 + logoTopOffset = 0.15 // Percent + logoLeftOffset = 0.03 // Percent usernameFontSize = 120.0 - usernameLeftOffset = 0.1 + usernameLeftOffset = 0.1 // Percent yearFontSize = 100.0 - yearLeftOffset = 0.85 + yearLeftOffset = 0.85 // Percent ) // Create3DText generates 3D text geometry for the username and year. @@ -189,26 +188,26 @@ func GenerateImageGeometry(innerWidth, baseHeight float64) ([]types.Triangle, er return nil, err } - config := imageRenderConfig{ - renderConfig: renderConfig{ - startX: innerWidth * logoPosition, - startY: -voxelDepth / 2.0, - startZ: -0.85 * baseHeight, - voxelScale: logoScale, - depth: voxelDepth, - }, - imagePath: imgPath, - height: logoHeight, - } - defer cleanup() - return renderImage(config) + return renderImage( + imgPath, + logoScale, + voxelDepth, + logoLeftOffset, + logoTopOffset, + ) } // renderImage generates 3D geometry for the given image configuration. -func renderImage(config imageRenderConfig) ([]types.Triangle, error) { - reader, err := os.Open(config.imagePath) +// func renderImage(config imageRenderConfig) ([]types.Triangle, error) { +func renderImage(filePath string, scale float64, height float64, leftOffsetPercent float64, topOffsetPercent float64) ([]types.Triangle, error) { + + faceWidthRes := skylineResolutionWidth + faceHeightRes := int(float64(faceWidthRes) * skylineFaceHeight/skylineFaceWidth) + + // Load image from file + reader, err := os.Open(filePath) if err != nil { return nil, errors.New(errors.IOError, "failed to open image", err) } @@ -219,34 +218,30 @@ func renderImage(config imageRenderConfig) ([]types.Triangle, error) { fmt.Println(closeErr) } }() - img, err := png.Decode(reader) if err != nil { return nil, errors.New(errors.IOError, "failed to decode PNG", err) } + // Get image size bounds := img.Bounds() - width := bounds.Max.X - height := bounds.Max.Y - - scale := config.height / float64(height) + logoWidth := bounds.Max.X + logoHeight := bounds.Max.Y + // Transfer image pixels onto face of skyline as voxels var triangles []types.Triangle - - for y := height - 1; y >= 0; y-- { - for x := 0; x < width; x++ { + for x := 0; x < logoWidth; x++ { + for y := logoHeight - 1; y >= 0; y-- { + // Get pixel color and alpha r, _, _, a := img.At(x, y).RGBA() + + // If pixel is active (white) and not fully transparent, create a voxel if a > 32768 && r > 32768 { - xPos := config.startX + float64(x)*config.voxelScale*scale - zPos := config.startZ + float64(height-1-y)*config.voxelScale*scale - - voxel, err := CreateCube( - xPos, - config.startY, - zPos, - config.voxelScale*scale, - config.depth, - config.voxelScale*scale, + + voxel, err := createVoxelOnFace( + (leftOffsetPercent * float64(faceWidthRes)) + float64(x)*logoScale, + (topOffsetPercent * float64(faceHeightRes)) + float64(y)*logoScale, + height, ) if err != nil { From beede5fc9917bdbd73e54ddff1b8dd41f95bdc1e Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Tue, 21 Jan 2025 15:30:10 +0000 Subject: [PATCH 3/7] refactor: Get base height and width using geometry package. --- stl/geometry/text.go | 69 +++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/stl/geometry/text.go b/stl/geometry/text.go index 3685e8c..c2155f7 100644 --- a/stl/geometry/text.go +++ b/stl/geometry/text.go @@ -27,13 +27,10 @@ type imageRenderConfig struct { } const ( - skylineFaceWidth = 142.5 // millimeters? - skylineFaceHeight = 10.0 // millimeters? - skylineResolutionWidth = 2000 // voxels - - voxelDepth = 1.0 + baseWidthVoxelResolution = 2000 // Number of voxels across the skyline face + voxelDepth = 1.0 // Distance to come out of face - logoScale = 0.4 + logoScale = 0.4 // Percent logoTopOffset = 0.15 // Percent logoLeftOffset = 0.03 // Percent @@ -45,7 +42,7 @@ const ( ) // Create3DText generates 3D text geometry for the username and year. -func Create3DText(username string, year string, innerWidth, baseHeight float64) ([]types.Triangle, error) { +func Create3DText(username string, year string, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { if username == "" { username = "anonymous" } @@ -54,6 +51,8 @@ func Create3DText(username string, year string, innerWidth, baseHeight float64) username, usernameLeftOffset, usernameFontSize, + baseWidth, + baseHeight, ) if err != nil { return nil, err @@ -63,6 +62,8 @@ func Create3DText(username string, year string, innerWidth, baseHeight float64) year, yearLeftOffset, yearFontSize, + baseWidth, + baseHeight, ) if err != nil { return nil, err @@ -78,17 +79,22 @@ func Create3DText(username string, year string, innerWidth, baseHeight float64) // Parameters: // text (string): The text to be displayed on the skyline's front face. // leftOffsetPercent (float64): The percentage distance from the left to start displaying the text. -// fontSize (float64): How large to make the text. Note: It scales with the skylineResolutionWidth. +// fontSize (float64): How large to make the text. Note: It scales with the baseWidthVoxelResolution. // // Returns: // ([]types.Triangle, error): A slice of triangles representing text. -func renderText(text string, leftOffsetPercent float64, fontSize float64) ([]types.Triangle, error) { +func renderText(text string, leftOffsetPercent float64, fontSize float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { // Create a rendering context for the face of the skyline - faceWidthRes := skylineResolutionWidth - faceHeightRes := int(float64(faceWidthRes) * skylineFaceHeight/skylineFaceWidth) + faceWidthRes := baseWidthVoxelResolution + faceHeightRes := int(float64(faceWidthRes) * baseHeight/baseWidth) + + // Create image representing the skyline face dc := gg.NewContext(faceWidthRes, faceHeightRes) + dc.SetRGB(0, 0, 0) + dc.Clear() + dc.SetRGB(1, 1, 1) - // Get temporary font file + // Load font into context fontPath, cleanup, err := writeTempFont(PrimaryFont) if err != nil { // Try fallback font @@ -97,27 +103,23 @@ func renderText(text string, leftOffsetPercent float64, fontSize float64) ([]typ return nil, errors.New(errors.IOError, "failed to load any fonts", err) } } - if err := dc.LoadFontFace(fontPath, fontSize); err != nil { return nil, errors.New(errors.IOError, "failed to load font", err) } - dc.SetRGB(0, 0, 0) - dc.Clear() - dc.SetRGB(1, 1, 1) - + // Draw text on image at desired location var triangles []types.Triangle // Draw pixelated text in image at desired location dc.DrawStringAnchored( text, - float64(faceWidthRes)*leftOffsetPercent, // Offset from left + float64(faceWidthRes)*leftOffsetPercent, // Offset from right float64(faceHeightRes)*0.5, // Offset from top 0.0, // Left aligned 0.5, // Vertically aligned ) - // Transfer image pixels onto face of skyline as voxels + // Convert context image pixels into voxels for x := 0; x < faceWidthRes; x++ { for y := 0; y < faceHeightRes; y++ { if isPixelActive(dc, x, y) { @@ -125,6 +127,8 @@ func renderText(text string, leftOffsetPercent float64, fontSize float64) ([]typ float64(x), float64(y), voxelDepth, + baseWidth, + baseHeight, ) if err != nil { return nil, errors.New(errors.STLError, "failed to create cube", err) @@ -151,19 +155,19 @@ func renderText(text string, leftOffsetPercent float64, fontSize float64) ([]typ // // Returns: // ([]types.Triangle, error): A slice of triangles representing the cube and an error if any. -func createVoxelOnFace(x float64, y float64, height float64) ([]types.Triangle, error) { +func createVoxelOnFace(x float64, y float64, height float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { // Mapping resolution - xResolution := float64(skylineResolutionWidth) - yResolution := xResolution * skylineFaceHeight / skylineFaceWidth + xResolution := float64(baseWidthVoxelResolution) + yResolution := xResolution * baseHeight / baseWidth // Pixel size voxelSize := 1.0; // Scale coordinate to face resolution - x = (x / xResolution) * skylineFaceWidth - y = (y / yResolution) * skylineFaceHeight - voxelSizeX := (voxelSize / xResolution) * skylineFaceWidth; - voxelSizeY := (voxelSize / yResolution) * skylineFaceHeight; + x = (x / xResolution) * baseWidth + y = (y / yResolution) * baseHeight + voxelSizeX := (voxelSize / xResolution) * baseWidth; + voxelSizeY := (voxelSize / yResolution) * baseHeight; cube, err := CreateCube( // Location (from top left corner of skyline face) @@ -181,7 +185,7 @@ func createVoxelOnFace(x float64, y float64, height float64) ([]types.Triangle, } // GenerateImageGeometry creates 3D geometry from the embedded logo image. -func GenerateImageGeometry(innerWidth, baseHeight float64) ([]types.Triangle, error) { +func GenerateImageGeometry(baseWidth float64, baseHeight float64) ([]types.Triangle, error) { // Get temporary image file imgPath, cleanup, err := getEmbeddedImage() if err != nil { @@ -196,15 +200,18 @@ func GenerateImageGeometry(innerWidth, baseHeight float64) ([]types.Triangle, er voxelDepth, logoLeftOffset, logoTopOffset, + baseWidth, + baseHeight, ) } // renderImage generates 3D geometry for the given image configuration. // func renderImage(config imageRenderConfig) ([]types.Triangle, error) { -func renderImage(filePath string, scale float64, height float64, leftOffsetPercent float64, topOffsetPercent float64) ([]types.Triangle, error) { +func renderImage(filePath string, scale float64, height float64, leftOffsetPercent float64, topOffsetPercent float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { - faceWidthRes := skylineResolutionWidth - faceHeightRes := int(float64(faceWidthRes) * skylineFaceHeight/skylineFaceWidth) + // Get voxel resolution of base face + faceWidthRes := baseWidthVoxelResolution + faceHeightRes := int(float64(faceWidthRes) * baseHeight/baseWidth) // Load image from file reader, err := os.Open(filePath) @@ -242,6 +249,8 @@ func renderImage(filePath string, scale float64, height float64, leftOffsetPerce (leftOffsetPercent * float64(faceWidthRes)) + float64(x)*logoScale, (topOffsetPercent * float64(faceHeightRes)) + float64(y)*logoScale, height, + baseWidth, + baseHeight, ) if err != nil { From cc764a83cbb6b9e7b98cf5ccdcab044930c5da19 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Tue, 21 Jan 2025 15:31:09 +0000 Subject: [PATCH 4/7] feat: Modify year text to be right justified --- stl/geometry/text.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/stl/geometry/text.go b/stl/geometry/text.go index c2155f7..aa3e8a0 100644 --- a/stl/geometry/text.go +++ b/stl/geometry/text.go @@ -35,10 +35,12 @@ const ( logoLeftOffset = 0.03 // Percent usernameFontSize = 120.0 + usernameJustification= "left" // "left", "center", "right" usernameLeftOffset = 0.1 // Percent yearFontSize = 100.0 - yearLeftOffset = 0.85 // Percent + yearJustification = "right" // "left", "center", "right" + yearLeftOffset = 0.97 // Percent ) // Create3DText generates 3D text geometry for the username and year. @@ -49,6 +51,7 @@ func Create3DText(username string, year string, baseWidth float64, baseHeight fl usernameTriangles, err := renderText( username, + usernameJustification, usernameLeftOffset, usernameFontSize, baseWidth, @@ -60,6 +63,7 @@ func Create3DText(username string, year string, baseWidth float64, baseHeight fl yearTriangles, err := renderText( year, + yearJustification, yearLeftOffset, yearFontSize, baseWidth, @@ -83,7 +87,7 @@ func Create3DText(username string, year string, baseWidth float64, baseHeight fl // // Returns: // ([]types.Triangle, error): A slice of triangles representing text. -func renderText(text string, leftOffsetPercent float64, fontSize float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { +func renderText(text string, justification string, leftOffsetPercent float64, fontSize float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { // Create a rendering context for the face of the skyline faceWidthRes := baseWidthVoxelResolution faceHeightRes := int(float64(faceWidthRes) * baseHeight/baseWidth) @@ -110,12 +114,22 @@ func renderText(text string, leftOffsetPercent float64, fontSize float64, baseWi // Draw text on image at desired location var triangles []types.Triangle - // Draw pixelated text in image at desired location + // Convert justification to a number + var justificationPercent float64 + switch justification { + case "center": + justificationPercent = 0.5 + case "right": + justificationPercent = 1.0 + default: + justificationPercent = 0.0 + } + dc.DrawStringAnchored( text, float64(faceWidthRes)*leftOffsetPercent, // Offset from right float64(faceHeightRes)*0.5, // Offset from top - 0.0, // Left aligned + justificationPercent, // Justification (0.0=left, 0.5=center, 1.0=right) 0.5, // Vertically aligned ) From 486e389ac3cf6652acaf4274524d861135f4b1e9 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Tue, 21 Jan 2025 15:48:49 +0000 Subject: [PATCH 5/7] feat: Update unit tests to match method inputs --- stl/geometry/text_test.go | 62 +++++++++++++++------------------------ 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/stl/geometry/text_test.go b/stl/geometry/text_test.go index c1500a7..4be3ecc 100644 --- a/stl/geometry/text_test.go +++ b/stl/geometry/text_test.go @@ -13,10 +13,6 @@ import ( // TestCreate3DText verifies text geometry generation functionality. func TestCreate3DText(t *testing.T) { - // Skip tests if fonts are not available - if _, err := os.Stat(FallbackFont); err != nil { - t.Skip("Skipping text tests as font files are not available") - } t.Run("verify basic text mesh generation", func(t *testing.T) { triangles, err := Create3DText("test", "2023", 100.0, 5.0) @@ -63,47 +59,37 @@ func TestCreate3DText(t *testing.T) { // TestRenderText verifies internal text rendering functionality func TestRenderText(t *testing.T) { - // Skip if fonts not available - if _, err := os.Stat(FallbackFont); err != nil { - t.Skip("Skipping text tests as font files are not available") - } + t.Run("verify text renders", func(t *testing.T) { + triangles, err := renderText( + "Mona", // text + "left", // justification + 0.1, // leftOffsetPercent + 10.0, // fontSize + 200.0, // baseWidth + 10.0, // baseHeight + ) - t.Run("verify text config validation", func(t *testing.T) { - invalidConfig := textRenderConfig{ - renderConfig: renderConfig{ - startX: 0, - startY: 0, - startZ: 0, - voxelScale: 0, // Invalid scale - depth: 1, - }, - text: "test", - contextWidth: 100, - contextHeight: 100, - fontSize: 10, - } - _, err := renderText(invalidConfig) - if err == nil { - t.Error("Expected error for invalid text config") + if err != nil { + t.Fatalf("renderText failed: %v", err) + } + if len(triangles) == 0 { + t.Error("Expected non-zero triangles for rendered text") } }) } // TestRenderImage verifies internal image rendering functionality func TestRenderImage(t *testing.T) { - t.Run("verify invalid image path", func(t *testing.T) { - config := imageRenderConfig{ - renderConfig: renderConfig{ - startX: 0, - startY: 0, - startZ: 0, - voxelScale: 1, - depth: 1, - }, - imagePath: "nonexistent.png", - height: 10, - } - _, err := renderImage(config) + t.Run("verify invalid image", func(t *testing.T) { + _, err := renderImage( + "nonexistent.png", // filePath + 0.5, // scale + 100.0, // height + 0.1, // leftOffsetPercent + 0.1, // topOffsetPercent + 200.0, // baseWidth + 10.0, // baseHeight + ) if err == nil { t.Error("Expected error for invalid image path") } From d220b6cfdb04bd7b1082cfd52b1b31c7cfed6c2e Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Tue, 21 Jan 2025 16:13:43 +0000 Subject: [PATCH 6/7] chore: cleanup from linting results --- stl/geometry/text.go | 91 +++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/stl/geometry/text.go b/stl/geometry/text.go index aa3e8a0..bee5245 100644 --- a/stl/geometry/text.go +++ b/stl/geometry/text.go @@ -10,37 +10,21 @@ import ( "github.com/github/gh-skyline/types" ) -// Common configuration for rendered elements -type renderConfig struct { - startX float64 - startY float64 - startZ float64 - voxelScale float64 - depth float64 -} - -// ImageConfig holds parameters for image rendering -type imageRenderConfig struct { - renderConfig - imagePath string - height float64 -} - const ( baseWidthVoxelResolution = 2000 // Number of voxels across the skyline face - voxelDepth = 1.0 // Distance to come out of face + voxelDepth = 1.0 // Distance to come out of face - logoScale = 0.4 // Percent + logoScale = 0.4 // Percent logoTopOffset = 0.15 // Percent logoLeftOffset = 0.03 // Percent - - usernameFontSize = 120.0 - usernameJustification= "left" // "left", "center", "right" - usernameLeftOffset = 0.1 // Percent - - yearFontSize = 100.0 - yearJustification = "right" // "left", "center", "right" - yearLeftOffset = 0.97 // Percent + + usernameFontSize = 120.0 + usernameJustification = "left" // "left", "center", "right" + usernameLeftOffset = 0.1 // Percent + + yearFontSize = 100.0 + yearJustification = "right" // "left", "center", "right" + yearLeftOffset = 0.97 // Percent ) // Create3DText generates 3D text geometry for the username and year. @@ -81,17 +65,19 @@ func Create3DText(username string, year string, baseWidth float64, baseHeight fl // It returns an array of types.Triangle. // // Parameters: -// text (string): The text to be displayed on the skyline's front face. -// leftOffsetPercent (float64): The percentage distance from the left to start displaying the text. -// fontSize (float64): How large to make the text. Note: It scales with the baseWidthVoxelResolution. +// +// text (string): The text to be displayed on the skyline's front face. +// leftOffsetPercent (float64): The percentage distance from the left to start displaying the text. +// fontSize (float64): How large to make the text. Note: It scales with the baseWidthVoxelResolution. // // Returns: -// ([]types.Triangle, error): A slice of triangles representing text. +// +// ([]types.Triangle, error): A slice of triangles representing text. func renderText(text string, justification string, leftOffsetPercent float64, fontSize float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { // Create a rendering context for the face of the skyline faceWidthRes := baseWidthVoxelResolution - faceHeightRes := int(float64(faceWidthRes) * baseHeight/baseWidth) - + faceHeightRes := int(float64(faceWidthRes) * baseHeight / baseWidth) + // Create image representing the skyline face dc := gg.NewContext(faceWidthRes, faceHeightRes) dc.SetRGB(0, 0, 0) @@ -128,9 +114,9 @@ func renderText(text string, justification string, leftOffsetPercent float64, fo dc.DrawStringAnchored( text, float64(faceWidthRes)*leftOffsetPercent, // Offset from right - float64(faceHeightRes)*0.5, // Offset from top - justificationPercent, // Justification (0.0=left, 0.5=center, 1.0=right) - 0.5, // Vertically aligned + float64(faceHeightRes)*0.5, // Offset from top + justificationPercent, // Justification (0.0=left, 0.5=center, 1.0=right) + 0.5, // Vertically aligned ) // Convert context image pixels into voxels @@ -163,35 +149,37 @@ func renderText(text string, justification string, leftOffsetPercent float64, fo // It returns a slice of types.Triangle representing the cube and an error if the cube creation fails. // // Parameters: -// x (float64): The x-coordinate on the skyline face (left to right). -// y (float64): The y-coordinate on the skyline face (top to bottom). -// height (float64): Distance coming out of the face. +// +// x (float64): The x-coordinate on the skyline face (left to right). +// y (float64): The y-coordinate on the skyline face (top to bottom). +// height (float64): Distance coming out of the face. // // Returns: -// ([]types.Triangle, error): A slice of triangles representing the cube and an error if any. +// +// ([]types.Triangle, error): A slice of triangles representing the cube and an error if any. func createVoxelOnFace(x float64, y float64, height float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { // Mapping resolution xResolution := float64(baseWidthVoxelResolution) yResolution := xResolution * baseHeight / baseWidth // Pixel size - voxelSize := 1.0; + voxelSize := 1.0 // Scale coordinate to face resolution x = (x / xResolution) * baseWidth y = (y / yResolution) * baseHeight - voxelSizeX := (voxelSize / xResolution) * baseWidth; - voxelSizeY := (voxelSize / yResolution) * baseHeight; + voxelSizeX := (voxelSize / xResolution) * baseWidth + voxelSizeY := (voxelSize / yResolution) * baseHeight cube, err := CreateCube( // Location (from top left corner of skyline face) - x, // x - Left to right - -height, // y - Negative comes out of face. Positive goes into face. - -voxelSizeY - y, // z - Bottom to top - + x, // x - Left to right + -height, // y - Negative comes out of face. Positive goes into face. + -voxelSizeY-y, // z - Bottom to top + // Size voxelSizeX, // x length - left to right from specified point - height, // thickness - distance coming out of face + height, // thickness - distance coming out of face voxelSizeY, // y length - bottom to top from specified point ) @@ -220,13 +208,12 @@ func GenerateImageGeometry(baseWidth float64, baseHeight float64) ([]types.Trian } // renderImage generates 3D geometry for the given image configuration. -// func renderImage(config imageRenderConfig) ([]types.Triangle, error) { func renderImage(filePath string, scale float64, height float64, leftOffsetPercent float64, topOffsetPercent float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) { // Get voxel resolution of base face faceWidthRes := baseWidthVoxelResolution - faceHeightRes := int(float64(faceWidthRes) * baseHeight/baseWidth) - + faceHeightRes := int(float64(faceWidthRes) * baseHeight / baseWidth) + // Load image from file reader, err := os.Open(filePath) if err != nil { @@ -260,8 +247,8 @@ func renderImage(filePath string, scale float64, height float64, leftOffsetPerce if a > 32768 && r > 32768 { voxel, err := createVoxelOnFace( - (leftOffsetPercent * float64(faceWidthRes)) + float64(x)*logoScale, - (topOffsetPercent * float64(faceHeightRes)) + float64(y)*logoScale, + (leftOffsetPercent*float64(faceWidthRes))+float64(x)*scale, + (topOffsetPercent*float64(faceHeightRes))+float64(y)*scale, height, baseWidth, baseHeight, From 6a347bc4613b4fb22371b0461db032d245355911 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Tue, 21 Jan 2025 16:17:31 +0000 Subject: [PATCH 7/7] chore: cleanup from linting results --- stl/geometry/text_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/stl/geometry/text_test.go b/stl/geometry/text_test.go index 4be3ecc..229487a 100644 --- a/stl/geometry/text_test.go +++ b/stl/geometry/text_test.go @@ -63,10 +63,10 @@ func TestRenderText(t *testing.T) { triangles, err := renderText( "Mona", // text "left", // justification - 0.1, // leftOffsetPercent - 10.0, // fontSize - 200.0, // baseWidth - 10.0, // baseHeight + 0.1, // leftOffsetPercent + 10.0, // fontSize + 200.0, // baseWidth + 10.0, // baseHeight ) if err != nil { @@ -83,12 +83,12 @@ func TestRenderImage(t *testing.T) { t.Run("verify invalid image", func(t *testing.T) { _, err := renderImage( "nonexistent.png", // filePath - 0.5, // scale - 100.0, // height - 0.1, // leftOffsetPercent - 0.1, // topOffsetPercent - 200.0, // baseWidth - 10.0, // baseHeight + 0.5, // scale + 100.0, // height + 0.1, // leftOffsetPercent + 0.1, // topOffsetPercent + 200.0, // baseWidth + 10.0, // baseHeight ) if err == nil { t.Error("Expected error for invalid image path")