Add the Neutral template family, template picker and text-only logo allowances
Linea (the vendor's original format.pdf design), Monogram and Serenity are rebuilt from the reference PDFs as one parametric family on the engine, with the user's logo in the measured slot, a home for every GST field and A4/Letter support. Adds static Montserrat, Open Sans and Poppins fonts and a generated Ms Madi outline for Serenity's vertical word. Text-only logo placeholders (Monolith, Tangerine) use a doc-derived allowance box. A Carbon TemplatePicker with committed thumbnails opens from the page setup controls.
This commit is contained in:
@@ -17,6 +17,10 @@ const logo = (over: Partial<SlotLogoInput>): SlotLogoInput => ({
|
||||
});
|
||||
const Sample_P = logo({ aspect: 4.62, density: 0.437, kind: "wordmark", includesName: true });
|
||||
|
||||
/** The box a logo is fitted to: the allowance for a text-only slot, else the lockup (or mark) box. */
|
||||
const lockupTarget = (s: TemplateSlot): Box => (s.textOnly ? s.allowance : s.lockup!);
|
||||
const markTarget = (s: TemplateSlot): Box => (s.textOnly ? s.allowance : (s.mark ?? s.lockup)!);
|
||||
|
||||
const inside = (inner: Box, outer: Box, eps = 1e-6) =>
|
||||
inner.x >= outer.x - eps && inner.y >= outer.y - eps && inner.x + inner.w <= outer.x + outer.w + eps && inner.y + inner.h <= outer.y + outer.h + eps;
|
||||
const closeBox = (a: Box, b: Box, tol = 0.5) => {
|
||||
@@ -39,6 +43,17 @@ describe("slots.generated", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("text-only placeholders (Monolith, Tangerine) carry a bigger allowance anchored at the text's edge", () => {
|
||||
expect(measured.filter(([, s]) => s.textOnly).map(([id]) => id).sort()).toEqual(["monolith", "tangerine-ledger"]);
|
||||
const mono = SLOTS.monolith!;
|
||||
expect(mono.allowance).toEqual({ x: 400, y: 54.75, w: 160, h: 28 });
|
||||
expect(mono.allowance.x + mono.allowance.w).toBeCloseTo(mono.lockup!.x + mono.lockup!.w, 6);
|
||||
const tang = SLOTS["tangerine-ledger"]!;
|
||||
expect(tang.allowance).toEqual({ x: 60.5, y: 62, w: 220, h: 36 });
|
||||
expect(tang.allowance.x).toBeCloseTo(tang.lockup!.x, 6);
|
||||
for (const [id, s] of measured) if (!s.textOnly) expect(s.allowance, id).toEqual(s.lockup);
|
||||
});
|
||||
|
||||
it("only Slate Band is a dark surface", () => {
|
||||
expect(measured.filter(([, s]) => s.darkSurface).map(([id]) => id)).toEqual(["slate-band"]);
|
||||
});
|
||||
@@ -63,7 +78,7 @@ describe("slots.optin", () => {
|
||||
describe("computeLogoPlacement identity: the placeholder itself comes back as its own box", () => {
|
||||
for (const [id, s] of measured) {
|
||||
it(`${id} lockup`, () => {
|
||||
const l = s.lockup!;
|
||||
const l = lockupTarget(s);
|
||||
const p = computeLogoPlacement({
|
||||
logo: logo({ aspect: l.w / l.h, density: s.lockupDensity, kind: "wordmark", includesName: true }),
|
||||
slot: s,
|
||||
@@ -71,7 +86,7 @@ describe("computeLogoPlacement identity: the placeholder itself comes back as it
|
||||
expect(p.slotKind).toBe("lockup");
|
||||
closeBox(p.box, l);
|
||||
});
|
||||
if (s.mark) {
|
||||
if (s.mark && !s.textOnly) {
|
||||
it(`${id} mark`, () => {
|
||||
const m = s.mark!;
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: s.markDensity }), slot: s });
|
||||
@@ -82,6 +97,48 @@ describe("computeLogoPlacement identity: the placeholder itself comes back as it
|
||||
}
|
||||
});
|
||||
|
||||
describe("computeLogoPlacement text-only allowances", () => {
|
||||
it("Test Vendor in Monolith is 28 pt tall, about 129 wide, and keeps the right edge", () => {
|
||||
const s = SLOTS.monolith!;
|
||||
const p = computeLogoPlacement({ logo: Sample_P, slot: s });
|
||||
expect(p.h).toBeCloseTo(28, 6);
|
||||
expect(p.w).toBeGreaterThanOrEqual(128);
|
||||
expect(p.w).toBeLessThanOrEqual(130);
|
||||
expect(p.x + p.w).toBeCloseTo(s.lockup!.x + s.lockup!.w, 6);
|
||||
expect(p.y).toBeCloseTo(s.allowance.y, 6);
|
||||
expect(p.slotKind).toBe("lockup");
|
||||
});
|
||||
|
||||
it("a very wide logo in Monolith is capped by the 160 pt width", () => {
|
||||
const s = SLOTS.monolith!;
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 10, density: 0.3, kind: "wordmark", includesName: true }), slot: s });
|
||||
expect(p.w).toBeCloseTo(160, 6);
|
||||
expect(p.h).toBeCloseTo(16, 6);
|
||||
expect(p.x + p.w).toBeCloseTo(560, 6);
|
||||
});
|
||||
|
||||
it("Test Vendor in Tangerine fills 36 pt of height (about 166 wide) at the left edge", () => {
|
||||
const s = SLOTS["tangerine-ledger"]!;
|
||||
const p = computeLogoPlacement({ logo: Sample_P, slot: s });
|
||||
expect(p.h).toBeCloseTo(36, 6);
|
||||
expect(p.w).toBeCloseTo(36 * 4.62, 6);
|
||||
expect(p.x).toBeCloseTo(60.5, 6);
|
||||
});
|
||||
|
||||
it("a square mark in Tangerine is 36 x 36 and vertically centred in the allowance", () => {
|
||||
const s = SLOTS["tangerine-ledger"]!;
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 1, density: 0.3 }), slot: s });
|
||||
expect(p.w).toBeCloseTo(36, 6);
|
||||
expect(p.y + p.h / 2).toBeCloseTo(62 + 18, 6);
|
||||
});
|
||||
|
||||
it("the weight factor still uses the text's density: a dense logo shrinks to 70 % at most", () => {
|
||||
const s = SLOTS["tangerine-ledger"]!;
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 4, density: 5, kind: "wordmark", includesName: true }), slot: s });
|
||||
expect(p.h).toBeCloseTo(36 * 0.7, 6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeLogoPlacement on every slot", () => {
|
||||
const all: [TemplateId, TemplateSlot][] = [...measured, ...optIn];
|
||||
|
||||
@@ -89,8 +146,8 @@ describe("computeLogoPlacement on every slot", () => {
|
||||
for (const [id, s] of all) {
|
||||
const p = computeLogoPlacement({ logo: Sample_P, slot: s });
|
||||
expect(p.slotKind).toBe("lockup");
|
||||
expect(inside(p.box, s.lockup!), id).toBe(true);
|
||||
if (!s.optIn) {
|
||||
expect(inside(p.box, lockupTarget(s)), id).toBe(true);
|
||||
if (!s.optIn && !s.textOnly) {
|
||||
const ink = p.w * p.h * Sample_P.density;
|
||||
const placeholderInk = s.lockup!.w * s.lockup!.h * s.lockupDensity;
|
||||
expect(ink, `${id}: ${ink.toFixed(1)} vs ${placeholderInk.toFixed(1)}`).toBeLessThanOrEqual(placeholderInk * 1.05);
|
||||
@@ -103,7 +160,7 @@ describe("computeLogoPlacement on every slot", () => {
|
||||
for (const [id, s] of all) {
|
||||
for (const l of shapes) {
|
||||
const p = computeLogoPlacement({ logo: l, slot: s });
|
||||
expect(inside(p.box, (s.mark ?? s.lockup)!), `${id} aspect ${l.aspect}`).toBe(true);
|
||||
expect(inside(p.box, markTarget(s)), `${id} aspect ${l.aspect}`).toBe(true);
|
||||
expect(p.w / p.h).toBeCloseTo(l.aspect, 6);
|
||||
}
|
||||
}
|
||||
@@ -114,7 +171,7 @@ describe("computeLogoPlacement on every slot", () => {
|
||||
expect(s.mark).toBeNull();
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 1 }), slot: s });
|
||||
expect(p.slotKind).toBe("lockup");
|
||||
expect(inside(p.box, s.lockup!)).toBe(true);
|
||||
expect(inside(p.box, s.allowance)).toBe(true);
|
||||
});
|
||||
|
||||
it("a denser logo is drawn smaller, at most down to 70 %, and a lighter one is not enlarged", () => {
|
||||
@@ -169,6 +226,8 @@ describe("computeLogoPlacement legibility floor", () => {
|
||||
anchor: "left",
|
||||
vAlign: "middle",
|
||||
darkSurface: false,
|
||||
textOnly: false,
|
||||
allowance: { x: 50, y: 50, w: 100, h },
|
||||
});
|
||||
|
||||
it("raises a logo that the weight factor made too short to 14 pt", () => {
|
||||
|
||||
@@ -91,12 +91,14 @@ function align(target: Box, w: number, h: number, slot: TemplateSlot): Box {
|
||||
/**
|
||||
* Places the user's logo where a template's logo placeholder is. Contain-fit in the measured box, then
|
||||
* a shrink-only weight factor so a denser logo draws smaller and matches the placeholder's visual
|
||||
* weight. A wordmark (includesName) replaces the whole lockup; a mark replaces the icon only. The
|
||||
* legibility floor never pushes past the box: a box under MIN_LOGO_HEIGHT stays the limit.
|
||||
* weight. A wordmark (includesName) replaces the whole lockup; a mark replaces the icon only. A
|
||||
* text-only placeholder (Monolith's "CAVEIN", Tangerine's "Rimberio") fits the template's allowance
|
||||
* instead, with the weight factor still computed against the text's density. The legibility floor never pushes past the box: a box under MIN_LOGO_HEIGHT stays the limit.
|
||||
*/
|
||||
export function computeLogoPlacement({ logo, slot, scale = 1, available }: SlotLogoPlacementInput): SlotLogoPlacement {
|
||||
const wantLockup = logo.includesName || !slot.mark;
|
||||
const target = wantLockup ? (slot.lockup ?? slot.mark) : slot.mark;
|
||||
// A text-only placeholder is too small for a logo; its allowance (from the template docs) is the target.
|
||||
const target = slot.textOnly ? slot.allowance : wantLockup ? (slot.lockup ?? slot.mark) : slot.mark;
|
||||
if (!target) throw new Error("slot has neither a mark nor a lockup box");
|
||||
const slotKind = wantLockup ? "lockup" : "mark";
|
||||
const placeholderDensity = wantLockup ? slot.lockupDensity : slot.markDensity;
|
||||
|
||||
Reference in New Issue
Block a user