(function () {
if (window.app) return; // desktop: preload bridge already present
// Dev defaults; production injects window.BL_PL_BASE / window.BL_GW_BASE
// (e.g. https://app.boxlogic.eu/trpc and https://files.boxlogic.eu).
var PL_BASE = (window.BL_PL_BASE || 'http://localhost:3000/trpc').replace(/\/+$/, '');
var GW_BASE = (window.BL_GW_BASE || 'http://localhost:8091').replace(/\/+$/, '');
var TOKEN_KEY = 'bl_portal_token';
function getToken() { try { return sessionStorage.getItem(TOKEN_KEY) || null; } catch (e) { return null; } }
function setToken(t) {
try { t ? sessionStorage.setItem(TOKEN_KEY, t) : sessionStorage.removeItem(TOKEN_KEY); } catch (e) {}
}
// tRPC envelope (verified live against PL): query = GET /?input={json},
// mutation = POST / {json}; success → result.data.json; error → error.json.
function unwrap(body) {
if (body && body.error) {
var e = body.error.json || body.error;
var err = new Error((e && e.message) || 'PrintLogic error');
err.code = e && e.data && e.data.code;
err.status = e && e.data && e.data.httpStatus;
throw err;
}
return body && body.result && body.result.data ? body.result.data.json : undefined;
}
async function call(method, proc, input, auth) {
var url = PL_BASE + '/' + proc;
var opts = { method: method, headers: { 'content-type': 'application/json' } };
var tok = getToken();
if (auth && tok) opts.headers['authorization'] = 'Bearer ' + tok;
if (method === 'GET') {
if (input !== undefined) url += '?input=' + encodeURIComponent(JSON.stringify({ json: input }));
} else {
opts.body = JSON.stringify({ json: input == null ? null : input });
}
var res = await fetch(url, opts);
var body = null;
try { body = await res.json(); } catch (e) {}
if (body && (body.error || body.result)) return unwrap(body);
var err = new Error('PrintLogic HTTP ' + res.status); err.status = res.status; throw err;
}
var q = function (p, i, a) { return call('GET', p, i, a); };
var m = function (p, i, a) { return call('POST', p, i, a); };
// Per-folder gateway token cache (PL mints 15-min tokens; reuse for ~13 min).
var tokCache = {};
async function mintFileToken(id) {
var c = tokCache[id];
if (c && (Date.now() - c.at) < 13 * 60 * 1000) return c.token;
var r = await m('boxLogicFile.mintForCustomer', { folderId: id }, true);
tokCache[id] = { token: r.token, at: Date.now() };
return r.token;
}
function mapProject(p) {
var t = p.type || {};
return {
id: p.folderId,
status: p.status,
title: p.title || null,
has3D: !!t.has3D,
customerEditable: !!t.customerEditable,
uploadOnly: !!t.uploadOnly,
cf2SelfServe: !!t.cf2SelfServe,
typeCode: t.code || null,
files: {}, // unknown at list time — the gateway is the source
configured: true, // customers never enter configure mode
isEmpty: false,
};
}
window.app = {
__portal: true,
login: async function (email, password) {
try {
var out = await m('customerAuth.login', { email: email, password: password }, false);
setToken(out.token);
return true;
} catch (e) {
setToken(null);
if (e && (e.status === 401 || e.code === 'UNAUTHORIZED')) return false;
throw e;
}
},
isAuthed: async function () {
if (!getToken()) return false;
try { await q('customerAuth.me', undefined, true); return true; }
catch (e) { setToken(null); return false; }
},
logout: async function () {
try { await m('customerAuth.logout', null, true); } catch (e) {}
setToken(null);
},
listProjects: async function () {
var rows = await q('boxLogicProject.listForCustomer', {}, true);
return (rows || []).map(mapProject);
},
// installAndSwitchTemplate uses this (portal only) → gateway URLs with a
// freshly-minted, folder-scoped token. Desktop preload has no fileUrls, so
// it keeps using the boxlogic:// cache protocol.
fileUrls: async function (id) {
var tok = await mintFileToken(id);
var u = function (name) { return GW_BASE + '/gw/' + id + '/' + name + '?token=' + encodeURIComponent(tok); };
return {
dieline: u(id + '_dieline.pdf'),
glb: u(id + '.glb'),
artwork: u(id + '_artwork.pdf'),
template: u(id + '_template.json'),
};
},
};
})();
Create dieline
Each part becomes its own artboard.
Name
Style
L (mm)
W (mm)
H (mm)
Bleed
Flute
2-side
L / W / H are the box's interior dimensions (what fits inside).
Sheets (0100) have no height — L / W are just the rectangle.
BoxLogic — new project
Pick the GLB and dieline PDF; the GLB is auto-analysed.
no file
Provide a dieline as a PDF, or as a CF2 (the server generates the PDF). Picking both is fine — the PDF is uploaded as-is and the CF2 is stored as source.
no file
no file
no files
Dieline PDF will be generated from the CF2(s) on the server.
Detected from GLB
—
CF2 → 3D part assignment happens in the viewer after upload.
PackagingProof
Awaiting PDF
Configure parts
Click a part on the 3D model.
Nesting
Pick a sheet and grain per piece, then click Calculate /sheet to nest.
Calculation
Part
Art
Die-lines loaded
Click Edit to add artwork in Affinity
Layers
Artwork offset
x: 0 y: 0
3D model:
Drag to rotate · Scroll to zoom
Rotate 90°
EngView render
Loading 3D model…
Applying artwork…
no file|—dieline.svg loaded — awaiting PDF upload
Add new template
Pieces:
Will load from: enter project number and template name above