Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22.14.0"

- name: Cache cargo registry
uses: actions/cache@v4
with:
Expand All @@ -45,7 +50,7 @@ jobs:
uses: actions/cache@v4
with:
path: wasm/target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('wasm/Cargo.lock') }}
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('rust-toolchain.toml', 'wasm/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-build-target-

Expand All @@ -54,28 +59,32 @@ jobs:
uses: actions/cache@v4
with:
path: wasm/pkg
key: ${{ runner.os }}-wasm-pkg-release-${{ hashFiles('scripts/vercel-build.sh', 'wasm/Cargo.lock', 'wasm/Cargo.toml', 'wasm/src/**') }}
key: ${{ runner.os }}-wasm-pkg-release-${{ hashFiles('scripts/vercel-build.sh', 'rust-toolchain.toml', 'wasm/Cargo.lock', 'wasm/Cargo.toml', 'wasm/src/**') }}

- name: Build or reuse WASM package
run: ./scripts/vercel-build.sh

- name: Check JavaScript syntax
run: |
for file in js/*.js; do
node --check "$file"
done
find js -type f -name '*.js' -print0 | sort -z | xargs -0 -n1 node --check

- name: Check renderer package and viewer helpers
run: npm run check --prefix packages/wasm-gerber-renderer

- name: Install Rust toolchain for tests
if: steps.wasm-pkg-cache.outputs.cache-hit != 'true'
run: |
rustup toolchain install stable --profile minimal
rustup default stable
rustup toolchain install 1.97.0 --profile minimal --component clippy --component rustfmt

- name: Run tests
if: steps.wasm-pkg-cache.outputs.cache-hit != 'true'
working-directory: ./wasm
run: cargo test --verbose

- name: Check Rust formatting
run: cargo fmt --manifest-path wasm/Cargo.toml --all -- --check

- name: Run Clippy
run: cargo clippy --manifest-path wasm/Cargo.toml --all-targets -- -D warnings

- name: Upload WASM package artifact
uses: actions/upload-artifact@v4
with:
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,12 @@ jobs:

- name: Set up Rust toolchain
run: |
rustup toolchain install stable --profile minimal
rustup default stable
rustup toolchain install 1.97.0 --profile minimal --component clippy --component rustfmt

- name: Check Rust formatting and lint
run: |
cargo fmt --manifest-path wasm/Cargo.toml --all -- --check
cargo clippy --manifest-path wasm/Cargo.toml --all-targets -- -D warnings

- name: Test Rust crate
run: cargo test --manifest-path wasm/Cargo.toml --verbose
Expand Down
54 changes: 48 additions & 6 deletions .github/workflows/renderer-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ on:
paths:
- ".github/workflows/renderer-compatibility.yml"
- "demo/*.gbr"
- "demo/*.drl"
- "packages/wasm-gerber-renderer/**"
- "scripts/assert-png-content.mjs"
- "scripts/vercel-build.sh"
- "rust-toolchain.toml"
- "wasm/**"
pull_request:
branches: ["main"]
paths:
- ".github/workflows/renderer-compatibility.yml"
- "demo/*.gbr"
- "demo/*.drl"
- "packages/wasm-gerber-renderer/**"
- "scripts/assert-png-content.mjs"
- "scripts/vercel-build.sh"
- "rust-toolchain.toml"
- "wasm/**"

permissions:
Expand Down Expand Up @@ -68,14 +74,22 @@ jobs:
include:
- platform: Linux x64
runner: ubuntu-latest
runtime_smoke: true
- platform: Linux arm64
runner: ubuntu-24.04-arm
runtime_smoke: true
- platform: macOS arm64
runner: macos-latest
runtime_smoke: true
- platform: macOS x64
runner: macos-15-intel
runtime_smoke: false
- platform: Windows x64
runner: windows-latest
runtime_smoke: true
- platform: Windows arm64
runner: windows-11-arm
runtime_smoke: true

defaults:
run:
Expand Down Expand Up @@ -114,6 +128,7 @@ jobs:
npm install "$package_file" node-gles-webgl2@latest

- name: Check native WebGL2 context
if: matrix.runtime_smoke != false
run: |
set -euo pipefail
cd "${RUNNER_TEMP}/renderer-smoke"
Expand Down Expand Up @@ -154,6 +169,7 @@ jobs:
NODE

- name: Render through CLI
if: matrix.runtime_smoke != false
run: |
set -euo pipefail
cd "${RUNNER_TEMP}/renderer-smoke"
Expand All @@ -169,33 +185,59 @@ jobs:
--height 360 \
--background '#05070c' \
--alpha 1
node -e "const { statSync } = require('node:fs'); const size = statSync('out-cli.png').size; if (size < 100) throw new Error('CLI PNG output is unexpectedly small'); console.log('CLI PNG bytes:', size);"
node "${GITHUB_WORKSPACE}/scripts/assert-png-content.mjs" out-cli.png '#05070c' 100 640 360

- name: Render through Node API
if: matrix.runtime_smoke != false
run: |
set -euo pipefail
cd "${RUNNER_TEMP}/renderer-smoke"
cat > smoke-node.mjs <<'NODE'
import { fileLayer, renderGerberToPngBuffer } from "wasm-gerber-renderer/node";
import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";

const png = await renderGerberToPngBuffer(
[fileLayer(resolve(process.env.GITHUB_WORKSPACE, "demo/gerber-feature-test.gbr"))],
[
fileLayer(resolve(process.env.GITHUB_WORKSPACE, "demo/gerber-feature-test.gbr")),
fileLayer(resolve(process.env.GITHUB_WORKSPACE, "demo/gerber-feature-test.gbr"), {
name: "inverted-mask.gbs",
inverted: true,
offsetX: 80,
}),
fileLayer(resolve(process.env.GITHUB_WORKSPACE, "demo/smoke-test.drl"), {
kind: "drill",
offsetX: 160,
}),
],
{
width: 640,
height: 360,
background: "#ffffff",
globalAlpha: 1,
invertedOutline: "bounds",
},
);

if (!(png instanceof Uint8Array) || png.length < 100) {
throw new Error("Node API PNG output is unexpectedly small");
}
console.log("Node API PNG bytes:", png.length);
if (!(png instanceof Uint8Array)) throw new Error("Node API did not return bytes");
await writeFile("out-node.png", png);
NODE
node_cmd=(node)
if [[ "${RUNNER_OS}" == "Linux" ]]; then
node_cmd=(xvfb-run -a node)
fi
"${node_cmd[@]}" smoke-node.mjs
node "${GITHUB_WORKSPACE}/scripts/assert-png-content.mjs" out-node.png '#ffffff' 100 640 360

- name: Validate build-only platform
if: matrix.runtime_smoke == false
run: |
set -euo pipefail
cd "${RUNNER_TEMP}/renderer-smoke"
node <<'NODE'
const nodeGles = require("node-gles-webgl2");
if (typeof nodeGles.createWebGLRenderingContext !== "function") {
throw new Error("node-gles-webgl2 native module did not load");
}
console.log("Native module loaded; runtime smoke is unavailable on this hosted runner");
NODE
9 changes: 9 additions & 0 deletions demo/smoke-test.drl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
M48
METRIC,LZ
T01C0.800
%
T01
X010000Y010000
X020000Y020000
X030000Y010000
M30
5 changes: 5 additions & 0 deletions js/core/config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export const NOTIFICATION_DURATION_MS = 2000;
export const MAX_FILE_SIZE_BYTES = 300 * 1024 * 1024;
export const MAX_ARCHIVE_ENTRY_COUNT = 1_000;
export const MAX_ARCHIVE_TOTAL_SIZE_BYTES = MAX_FILE_SIZE_BYTES;
export const MAX_ARCHIVE_COMPRESSION_RATIO = 1_000;
export const MAX_SOURCE_REPEAT = 100;
export const MAX_LAYER_COUNT = 500;
export const MAX_SCREENSHOT_STREAM_BAND_BYTES = 1024 * 1024 * 1024;
export const MAX_SCREENSHOT_RENDER_TARGET_BYTES = 2 * 1024 * 1024 * 1024;

Expand Down
80 changes: 41 additions & 39 deletions js/core/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1376,7 +1376,7 @@ export class GerberViewer {

this.fileInput.addEventListener("change", (e) => {
if (e.target.files.length > 0) {
this.handleFileUpload(e.target.files);
this.startFileUpload(e.target.files);
}
});

Expand Down Expand Up @@ -3125,10 +3125,9 @@ export class GerberViewer {
this.isInitialUrlLoading = false;
return;
}
const repeat = getInitialSourceRepeat();
const repeatOffset = getInitialSourceRepeatOffset();

try {
const repeat = getInitialSourceRepeat();
const repeatOffset = getInitialSourceRepeatOffset();
this.isInitialUrlLoading = true;
this.updateUiState();
const url = new URL(sourceUrl);
Expand Down Expand Up @@ -3193,38 +3192,38 @@ export class GerberViewer {
return;
}

const oversizedFiles = [];
const validFiles = [];

this.setWorkspaceStatus("Loading files");

// Validate file sizes
for (const file of files) {
if (file.size > MAX_FILE_SIZE_BYTES) {
oversizedFiles.push({
name: file.name,
size: formatFileSize(file.size),
limit: formatFileSize(MAX_FILE_SIZE_BYTES),
});
} else {
validFiles.push(file);
try {
const oversizedFiles = [];
const validFiles = [];

this.setWorkspaceStatus("Loading files");

// Validate file sizes
for (const file of files) {
if (file.size > MAX_FILE_SIZE_BYTES) {
oversizedFiles.push({
name: file.name,
size: formatFileSize(file.size),
limit: formatFileSize(MAX_FILE_SIZE_BYTES),
});
} else {
validFiles.push(file);
}
}
}

// Show warning for oversized files
if (oversizedFiles.length > 0) {
this.showFileSizeWarning(oversizedFiles);
}
// Show warning for oversized files
if (oversizedFiles.length > 0) {
this.showFileSizeWarning(oversizedFiles);
}

if (validFiles.length > 0) {
this.showLoadingModal({
title: "Loading files",
stage: "Preparing",
current: 0,
total: validFiles.length,
});
if (validFiles.length > 0) {
this.showLoadingModal({
title: "Loading files",
stage: "Preparing",
current: 0,
total: validFiles.length,
});

try {
const layerSources = await this.collectLayerSources(validFiles);

if (layerSources.length > 0) {
Expand All @@ -3240,15 +3239,18 @@ export class GerberViewer {
this.addDiagnostic("info", "Files loaded", `${loadedCount} processed`);
}
}
} finally {
this.hideLoadingModal();
}
} finally {
this.hideLoadingModal();
this.updateUiState();
this.fileInput.value = "";
}
}

this.updateUiState();

// Clear file input
this.fileInput.value = "";
startFileUpload(files) {
void this.handleFileUpload(files).catch((error) => {
this.handleLayerLoadError("File upload", error);
});
}

async loadLayerSources(layerSources, { title = "Loading files" } = {}) {
Expand Down Expand Up @@ -7043,7 +7045,7 @@ export class GerberViewer {

const files = e.dataTransfer?.files;
if (files?.length > 0) {
this.handleFileUpload(files);
this.startFileUpload(files);
}
}

Expand Down
Loading
Loading