feat(gate-b): Gate B mask + BDFT + length axis (#21–#23) #31

Closed
grokbot_ty wants to merge 41 commits from gate-b into main
12 changed files with 2938 additions and 171 deletions
Showing only changes of commit 8d8b3a2ff1 - Show all commits
+9 -5
View File
@@ -7,11 +7,15 @@ RUN npm ci --ignore-scripts
COPY frontend/ . COPY frontend/ .
# Build the SvelteKit app (stub for Gate B — will be updated when SvelteKit scaffolds) RUN npm run build
# For now, copy a stub HTML page. Vite build runs in production mode later.
RUN mkdir -p build && \
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>SlabUploader</title></head><body><h1>SlabUploader — coming soon</h1><p>SvelteKit app stub. Update this Dockerfile when the SvelteKit app is scaffolded.</p></body></html>' > build/index.html
# adapter-static outputs to build/
EXPOSE 80 EXPOSE 80
CMD ["node", "-e", "const h=require('http'),fs=require('fs'),url=require('url');h.createServer((req,res)=>{const f='build/index.html';res.writeHead(200,{'Content-Type':'text/html'}).end(fs.readFileSync(f))}).listen(80);console.log('frontend on :80')"] CMD ["node", "-e", "\
const h=require('http'),fs=require('fs'),url=require('url'),path=require('path');\
const root='build';\
const types={'/':'application/json','js':'application/javascript','css':'text/css','html':'text/html','json':'application/json','svg':'image/svg+xml','png':'image/png','jpg':'image/jpeg','jpeg':'image/jpeg','ico':'image/x-icon','woff2':'font/woff2','woff':'font/woff','ttf':'font/ttf','otf':'font/otf','map':'application/json'};\
h.createServer((req,res)=>{const p=url.parse(req.url||'/').pathname||'/';const fp=path.join(root,p==='/'?'/index.html':p);try{const s=fs.statSync(fp);if(s.isDirectory()){fs.readFile(path.join(fp,'index.html'),'utf8',(e,d)=>{res.writeHead(200,{'Content-Type':'text/html'});res.end(d||'<h1>OK</h1>')})}else{const ext=path.extname(fp).slice(1);res.writeHead(200,{'Content-Type':types[ext]||'application/octet-stream'});fs.createReadStream(fp).pipe(res)}}catch(e){res.writeHead(404).end('Not found')}}).listen(80);\
console.log('frontend static build on :80');\
"]
+1640 -163
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -4,12 +4,21 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest" "test:watch": "vitest"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.1.1",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^6.0.7",
"vitest": "^2.1.4" "vitest": "^2.1.4"
} }
} }
+14
View File
@@ -0,0 +1,14 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for an explanation of these declarations
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SlabUploader</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="false">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+93
View File
@@ -0,0 +1,93 @@
/** App state for the capture → mask → axis → numbers → review flow.
* Uses Svelte 5 runes ($state, $derived) — no svelte/store dependency.
*/
import type {
RgbaImage,
MaskBuffer,
MaskResult,
KnobSettings,
AxisSession,
LengthAxis,
WidthStats,
CropResult,
} from '$lib/pipeline/types.js';
import type { SkuValidationResult } from '$lib/validation/sku.js';
import { defaultKnobs } from '$lib/pipeline/mask.js';
import { validateSku, isSkuValid } from '$lib/validation/sku.js';
/** Steps in the flow. */
export type AppStep = 'capture' | 'mask' | 'axis' | 'numbers' | 'review';
export interface PhotoEntry {
file: File;
blob: Blob;
rgba: RgbaImage | null;
maskBuffer: MaskBuffer | null;
axis: LengthAxis | null;
cropResult: CropResult | null;
widthStats: WidthStats | null;
lengthIn: number | null;
knobSettings: KnobSettings;
maskResult: MaskResult | null;
axisSession: AxisSession | null;
}
export interface ReviewData {
id: string;
serverRev: number;
wooProductId: number | null;
wooAdminUrl: string | null;
}
export interface SlabState {
step: AppStep;
photos: PhotoEntry[];
currentPhotoIndex: number;
lengthIn: number;
thicknessIn: number;
sku: string;
skuValidation: SkuValidationResult;
sqft: number;
bdft: number;
widthStats: WidthStats | null;
cropResult: CropResult | null;
meetsMinPx: boolean;
uploaded: boolean;
uploadError: string | null;
uploading: boolean;
reviewData: ReviewData | null;
}
export function createSlabState(): SlabState {
return {
step: 'capture',
photos: [],
currentPhotoIndex: 0,
lengthIn: 0,
thicknessIn: 0,
sku: '',
skuValidation: { valid: false, errors: ['too_short'] },
sqft: 0,
bdft: 0,
widthStats: null,
cropResult: null,
meetsMinPx: true,
uploaded: false,
uploadError: null,
uploading: false,
reviewData: null,
};
}
export function setSku(sku: string): SkuValidationResult {
return validateSku(sku);
}
export function computeSqft(lengthIn: number, widthPx: number, scalePx: number): number {
const widthIn = widthPx / scalePx;
return (lengthIn * widthIn) / 144;
}
export function computeBdft(sqft: number, thicknessIn: number): number {
return sqft * thicknessIn;
}
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts">
import type { LayoutData } from './$types.js';
export let data: LayoutData;
</script>
<h1 class="app-header">SlabUploader</h1>
<slot />
<style>
:global(*) {
box-sizing: border-box;
}
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
color: #1a1a1a;
}
:global(.app-wrapper) {
max-width: 960px;
margin: 0 auto;
}
.app-header {
text-align: center;
padding: 1rem;
background: #1a1a1a;
color: white;
margin: 0;
font-size: 1.5rem;
letter-spacing: 0.05em;
}
</style>
+6
View File
@@ -0,0 +1,6 @@
export const ssr = false;
export const prerender = true;
export function load() {
return {};
}
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
paths: {
base: ''
}
}
};
export default config;
+9 -3
View File
@@ -5,9 +5,15 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"strict": true, "strict": true,
"noEmit": true, "noEmit": true,
"skipLibCheck": true,
"types": ["vitest/globals"], "types": ["vitest/globals"],
"lib": ["ES2022"] "lib": ["ES2022", "DOM", "DOM.Iterable"],
"paths": {
"$lib/*": ["src/lib/*"],
"$app/*": [".svelte-kit/types/$app/*"],
"$types": [".svelte-kit/types/*"]
},
"baseUrl": "."
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts", "src/**/*.svelte"],
"exclude": ["node_modules"]
} }
+6
View File
@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});