@hraness/slopcamera 3.3.4 → 3.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/args.ts +61 -0
- package/apps/desktop/cli/capability-manifest.ts +4 -1
- package/apps/desktop/cli/command-host-resources.ts +7 -0
- package/apps/desktop/cli/commands.ts +550 -2
- package/apps/desktop/cli/credits.ts +818 -0
- package/apps/desktop/cli/help.ts +47 -4
- package/apps/desktop/dist/cli/main.js +327 -253
- package/dist/cli.js +14 -4
- package/dist/index-10bz03js.js +28 -0
- package/dist/{index-296qhpf2.js → index-qtn3s421.js} +1 -1
- package/dist/{index-0bbesn4m.js → index-sy1n4zfy.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/operations.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/README.md +1 -0
- package/package.json +1 -1
- package/schema/drawing.schema.json +27 -0
- package/skills/slopcamera/SKILL.md +1 -0
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/patent-drawings.md +53 -0
- package/src/cli.ts +90 -2
- package/src/drawing-pdf.ts +210 -0
- package/src/drawing.ts +293 -0
- package/src/index.ts +19 -0
- package/src/render.ts +6 -3
- package/src/version.ts +1 -1
- package/dist/index-c8jpst4g.js +0 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import{VectorizeDeadline,encodeTracePng,loadRaster,normalizedHexColor,parseHexColor,resolveVectorizeLimits,vectorizeHardLimits,vectorizeImage}from"./index-qry58nj2.js";import{SlopcameraCloudError,createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraImageModels,slopcameraMaximumPromptBytes}from"./index-77fjfg9f.js";import{createDefaultHostResourceCoordinator}from"./index-b3geqczn.js";import{__require}from"./index-80a36bc0.js";import{randomUUID as randomUUID3}from"crypto";import{mkdir as mkdir3,readFile as readFile2,rename as rename2,rm as rm3,writeFile as writeFile3}from"fs/promises";import{dirname as dirname3,join as join2,resolve as resolve3}from"path";var shared='fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"';var builtInIcons=Object.freeze({brain:{viewBox:"0 0 24 24",body:`<path ${shared} d="M9.5 4.5A3.5 3.5 0 0 0 6 8v.35A3.5 3.5 0 0 0 5.5 15 3.5 3.5 0 0 0 9 19.5M14.5 4.5A3.5 3.5 0 0 1 18 8v.35a3.5 3.5 0 0 1 .5 6.65 3.5 3.5 0 0 1-3.5 4.5M9.5 4.5v15M14.5 4.5v15M6 9.5h3.5M14.5 14.5H18"/>`},check:{viewBox:"0 0 24 24",body:`<path ${shared} d="m5 12 4.25 4.25L19 6.5"/>`},code:{viewBox:"0 0 24 24",body:`<path ${shared} d="m8.5 7-5 5 5 5M15.5 7l5 5-5 5M13.5 4l-3 16"/>`},database:{viewBox:"0 0 24 24",body:`<ellipse ${shared} cx="12" cy="5" rx="7.5" ry="3"/><path ${shared} d="M4.5 5v7c0 1.66 3.36 3 7.5 3s7.5-1.34 7.5-3V5M4.5 12v7c0 1.66 3.36 3 7.5 3s7.5-1.34 7.5-3v-7"/>`},document:{viewBox:"0 0 24 24",body:`<path ${shared} d="M6 2.75h8l4 4V21.25H6zM14 2.75v4h4M9 11h6M9 15h6"/>`},globe:{viewBox:"0 0 24 24",body:`<circle ${shared} cx="12" cy="12" r="9"/><path ${shared} d="M3 12h18M12 3c2.4 2.45 3.5 5.45 3.5 9S14.4 18.55 12 21M12 3C9.6 5.45 8.5 8.45 8.5 12s1.1 6.55 3.5 9"/>`},search:{viewBox:"0 0 24 24",body:`<circle ${shared} cx="10.5" cy="10.5" r="6.5"/><path ${shared} d="m15.25 15.25 5 5"/>`},server:{viewBox:"0 0 24 24",body:`<rect ${shared} x="3" y="3.5" width="18" height="7" rx="2"/><rect ${shared} x="3" y="13.5" width="18" height="7" rx="2"/><path ${shared} d="M7 7h.01M7 17h.01M11 7h6M11 17h6"/>`},shield:{viewBox:"0 0 24 24",body:`<path ${shared} d="M12 2.75 20 6v5.5c0 5.25-3.35 8.2-8 10-4.65-1.8-8-4.75-8-10V6z"/>`},user:{viewBox:"0 0 24 24",body:`<circle ${shared} cx="12" cy="8" r="4"/><path ${shared} d="M4.5 21c.5-4.2 3-6.5 7.5-6.5s7 2.3 7.5 6.5"/>`}});function sanitizeIcon(icon){const dangerous=/<\s*(script|style|foreignObject|iframe|object|embed|image|use|a)\b|on[a-z]+\s*=|(?:xlink:)?href\s*=|style\s*=|javascript:|url\s*\(/i;if(dangerous.test(icon.body)){throw new Error("Icon body contains executable or externally embedded SVG content")}if(!/^[-+.\d\s]+$/.test(icon.viewBox.trim())){throw new Error(`Invalid icon viewBox: ${icon.viewBox}`)}return icon}var defaultLabelFontSize=22;var defaultLabelWeight=600;var defaultLineHeightRatio=1.35;var defaultRowGap=8;var iconLabelGap=12;function characterWidthRatio(fontFamily){return fontFamily==="mono"?0.62:0.56}function estimateDiagramTextWidth(text,fontSize,fontFamily="default"){return text.length*fontSize*characterWidthRatio(fontFamily)}function wrapDiagramText(text,maxWidth,fontSize,fontFamily="default"){const explicitLines=text.split(`
|
|
3
|
-
`);const maxCharacters=Math.max(1,Math.floor(maxWidth/(fontSize*characterWidthRatio(fontFamily))));const lines=[];for(const explicitLine of explicitLines){if(explicitLine.length<=maxCharacters){lines.push(explicitLine);continue}const words=explicitLine.split(/\s+/);let current="";for(const word of words){const candidate=current===""?word:`${current} ${word}`;if(candidate.length<=maxCharacters||current===""){current=candidate}else{lines.push(current);current=word}}if(current!=="")lines.push(current)}return lines.length===0?[""]:lines}function resolveRows(shape){if(shape.labelRows!==undefined){return shape.labelRows.map((row)=>({text:row.text,fontSize:row.fontSize??defaultLabelFontSize,fontFamily:row.fontFamily??"default",weight:row.weight??defaultLabelWeight,lineHeight:(row.fontSize??defaultLabelFontSize)*defaultLineHeightRatio}))}if(shape.label===undefined)return[];const fontSize=shape.labelFontSize??defaultLabelFontSize;return[{text:shape.label,fontSize,fontFamily:shape.labelFontFamily??"default",weight:shape.labelWeight??defaultLabelWeight,lineHeight:fontSize*defaultLineHeightRatio}]}function layoutBoxContent(shape,resolvedIconSize){const textWidth=Math.max(1,shape.width-32);const pendingRows=resolveRows(shape).map((row)=>{const lines=wrapDiagramText(row.text,textWidth,row.fontSize,row.fontFamily);const height=lines.length*row.lineHeight;return{...row,lines,height}});const rowGap=shape.labelRows===undefined?0:shape.labelRowGap??defaultRowGap;const rowsHeight=pendingRows.reduce((total,row)=>total+row.height,0)+Math.max(0,pendingRows.length-1)*rowGap;const hasIcon=resolvedIconSize!==undefined;const gap=hasIcon&&pendingRows.length>0?iconLabelGap:0;const contentHeight=(resolvedIconSize??0)+gap+rowsHeight;let cursorY=shape.y+(shape.height-contentHeight)/2;const icon=resolvedIconSize===undefined?undefined:{x:shape.x+(shape.width-resolvedIconSize)/2,y:cursorY,size:resolvedIconSize};if(hasIcon)cursorY+=resolvedIconSize+gap;const rows=pendingRows.map((row)=>{const resolved={...row,y:cursorY};cursorY+=row.height+rowGap;return resolved});return{...icon===undefined?{}:{icon},rows}}import{readFile}from"fs/promises";import{extname,isAbsolute}from"path";import{fileURLToPath}from"url";import{Resvg}from"@resvg/resvg-js";var NebulaSans_Bold_default="./NebulaSans-Bold-26se8aek.otf";var NebulaSans_Bold_default2="./NebulaSans-Bold-bcz7y08t.woff2";var NebulaSans_Book_default="./NebulaSans-Book-8cenzchw.otf";var NebulaSans_Book_default2="./NebulaSans-Book-5ax05zvn.woff2";var tones=["neutral","blue","orange","green","red","purple","yellow"];var defaults={light:{background:"#ffffff",foreground:"#18181b",muted:"#71717a",stroke:"#27272a",tones:{neutral:{fill:"#f4f4f5",stroke:"#52525b",text:"#18181b"},blue:{fill:"#dbeafe",stroke:"#2563eb",text:"#1e3a8a"},orange:{fill:"#ffedd5",stroke:"#ea580c",text:"#7c2d12"},green:{fill:"#dcfce7",stroke:"#16a34a",text:"#14532d"},red:{fill:"#fee2e2",stroke:"#dc2626",text:"#7f1d1d"},purple:{fill:"#f3e8ff",stroke:"#9333ea",text:"#581c87"},yellow:{fill:"#fef9c3",stroke:"#ca8a04",text:"#713f12"}}},dark:{background:"#09090b",foreground:"#fafafa",muted:"#a1a1aa",stroke:"#d4d4d8",tones:{neutral:{fill:"#27272a",stroke:"#a1a1aa",text:"#fafafa"},blue:{fill:"#172554",stroke:"#60a5fa",text:"#dbeafe"},orange:{fill:"#431407",stroke:"#fb923c",text:"#ffedd5"},green:{fill:"#052e16",stroke:"#4ade80",text:"#dcfce7"},red:{fill:"#450a0a",stroke:"#f87171",text:"#fee2e2"},purple:{fill:"#3b0764",stroke:"#c084fc",text:"#f3e8ff"},yellow:{fill:"#422006",stroke:"#facc15",text:"#fef9c3"}}}};function mergeTheme(base,override){if(override===undefined)return base;return{background:override.background??base.background,foreground:override.foreground??base.foreground,muted:override.muted??base.muted,stroke:override.stroke??base.stroke,tones:Object.fromEntries(tones.map((tone)=>[tone,{fill:override.tones?.[tone]?.fill??base.tones[tone].fill,stroke:override.tones?.[tone]?.stroke??base.tones[tone].stroke,text:override.tones?.[tone]?.text??base.tones[tone].text}]))}}function resolveTheme(mode,config){return mergeTheme(defaults[mode],config.theme?.[mode])}var bundledAssetPath=(asset)=>isAbsolute(asset)?asset:fileURLToPath(new URL(asset,import.meta.url));var defaultFont=Object.freeze({family:"Nebula Sans",files:Object.freeze([Object.freeze({embed:true,path:bundledAssetPath(NebulaSans_Book_default2),style:"normal",weight:400}),Object.freeze({embed:true,path:bundledAssetPath(NebulaSans_Bold_default2),style:"normal",weight:700}),Object.freeze({embed:false,path:bundledAssetPath(NebulaSans_Book_default),style:"normal",weight:400}),Object.freeze({embed:false,path:bundledAssetPath(NebulaSans_Bold_default),style:"normal",weight:700})])});var escapeXml=(value)=>value.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");function fontMime(filePath){switch(extname(filePath).toLowerCase()){case".otf":return"font/otf";case".woff":return"font/woff";case".woff2":return"font/woff2";default:return"font/ttf"}}async function fontCss(font){if(font.files===undefined)return"";const faces=await Promise.all(font.files.filter((file)=>file.embed===true).map(async(file)=>{const encoded=(await readFile(file.path)).toString("base64");return`@font-face{font-family:"${escapeXml(font.family)}";src:url(data:${fontMime(file.path)};base64,${encoded});font-weight:${file.weight??400};font-style:${file.style??"normal"};}`}));return faces.join("")}function textSvg(options){const lines=options.lines??wrapDiagramText(options.text,options.width,options.fontSize);const lineHeight=options.lineHeight??options.fontSize*1.25;const anchor=options.align;const x=anchor==="middle"?options.x+options.width/2:anchor==="end"?options.x+options.width:options.x;const dominantBaseline=options.centerLineBoxes?"central":"hanging";return`<text x="${x}" y="${options.y}" text-anchor="${anchor}" dominant-baseline="${dominantBaseline}" fill="${options.color}" opacity="${options.opacity}" font-family="${escapeXml(options.family)}" font-size="${options.fontSize}" font-weight="${options.weight}">${lines.map((line,index)=>options.centerLineBoxes?`<tspan x="${x}" y="${options.y+(index+0.5)*lineHeight}">${escapeXml(line)}</tspan>`:`<tspan x="${x}" dy="${index===0?0:lineHeight}">${escapeXml(line)}</tspan>`).join("")}</text>`}function iconSvg(options){const icon=sanitizeIcon(options.icon);const parts=icon.viewBox.trim().split(/\s+/).map(Number);const width=(parts[2]??24)-(parts[0]??0);const height=(parts[3]??24)-(parts[1]??0);const scale=options.size/Math.max(width,height);return`<g color="${options.color}" opacity="${options.opacity}" transform="translate(${options.x} ${options.y}) scale(${scale}) translate(${-Number(parts[0]??0)} ${-Number(parts[1]??0)})">${icon.body}</g>`}function boxSvg(shape,theme,families,icons){const tone=theme.tones[shape.tone??"neutral"];const strokeWidth=shape.strokeWidth??2;const opacity=shape.opacity??1;const radius=shape.type==="ellipse"?0:Math.min(shape.radius??22,shape.height/2);const geometry=shape.type==="ellipse"?`<ellipse cx="${shape.x+shape.width/2}" cy="${shape.y+shape.height/2}" rx="${shape.width/2}" ry="${shape.height/2}" fill="${shape.fill===false?"none":tone.fill}" stroke="${tone.stroke}" stroke-width="${strokeWidth}"/>`:`<rect x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}" rx="${radius}" fill="${shape.fill===false?"none":tone.fill}" stroke="${tone.stroke}" stroke-width="${strokeWidth}"/>`;const icon=shape.icon===undefined?undefined:icons[shape.icon];if(shape.icon!==undefined&&icon===undefined){throw new Error(`Unknown icon "${shape.icon}" on shape ${shape.id}`)}const iconSize=Math.min(shape.iconSize??52,shape.height*0.45,shape.width*0.32);const content=layoutBoxContent(shape,icon===undefined?undefined:iconSize);const iconMarkup=icon===undefined||content.icon===undefined?"":iconSvg({icon,x:content.icon.x,y:content.icon.y,size:content.icon.size,color:tone.text,opacity:1});const labelMarkup=content.rows.map((row)=>textSvg({text:row.text,x:shape.x+16,y:row.y,width:shape.width-32,fontSize:row.fontSize,weight:row.weight,align:"middle",color:tone.text,opacity:1,family:families[row.fontFamily],lineHeight:row.lineHeight,lines:row.lines,centerLineBoxes:true})).join("");return`<g data-shape-id="${escapeXml(shape.id)}" opacity="${opacity}">${geometry}${iconMarkup}${labelMarkup}</g>`}function pointForAnchor(shape,anchor,toward,position){const center={x:shape.x+shape.width/2,y:shape.y+shape.height/2};let resolved=anchor??"auto";if(resolved==="auto"){const dx=toward.x-center.x;const dy=toward.y-center.y;resolved=Math.abs(dx/shape.width)>=Math.abs(dy/shape.height)?dx>=0?"right":"left":dy>=0?"bottom":"top"}const resolvedPosition=position??0.5;if(shape.type==="ellipse"){const projected=(resolvedPosition-0.5)*2;const radial=Math.sqrt(Math.max(0,1-projected*projected))/2;const normalized=resolved==="top"?{x:resolvedPosition,y:0.5-radial}:resolved==="bottom"?{x:resolvedPosition,y:0.5+radial}:resolved==="left"?{x:0.5-radial,y:resolvedPosition}:{x:0.5+radial,y:resolvedPosition};return{x:shape.x+shape.width*normalized.x,y:shape.y+shape.height*normalized.y,normalized}}switch(resolved){case"top":return{x:shape.x+shape.width*resolvedPosition,y:shape.y,normalized:{x:resolvedPosition,y:0}};case"right":return{x:shape.x+shape.width,y:shape.y+shape.height*resolvedPosition,normalized:{x:1,y:resolvedPosition}};case"bottom":return{x:shape.x+shape.width*resolvedPosition,y:shape.y+shape.height,normalized:{x:resolvedPosition,y:1}};case"left":return{x:shape.x,y:shape.y+shape.height*resolvedPosition,normalized:{x:0,y:resolvedPosition}}}}function resolveEdge(spec,edge){const boxes=new Map(spec.shapes.filter((shape)=>shape.type==="rect"||shape.type==="ellipse").map((shape)=>[shape.id,shape]));const from=boxes.get(edge.from);const to=boxes.get(edge.to);if(from===undefined||to===undefined){throw new Error(`Edge ${edge.id} references a missing box`)}const fromCenter={x:from.x+from.width/2,y:from.y+from.height/2};const toCenter={x:to.x+to.width/2,y:to.y+to.height/2};const start=pointForAnchor(from,edge.start,toCenter,edge.startPosition);const end=pointForAnchor(to,edge.end,fromCenter,edge.endPosition);const dx=end.x-start.x;const dy=end.y-start.y;const length=Math.hypot(dx,dy)||1;const bend=edge.bend??0;return{edge,from,to,start,end,control:{x:(start.x+end.x)/2+-dy/length*bend,y:(start.y+end.y)/2+dx/length*bend}}}function resolveEdgeLabel(resolved){const{edge,start,end,control}=resolved;const t=edge.labelPosition??0.5;const oneMinusT=1-t;const point={x:oneMinusT*oneMinusT*start.x+2*oneMinusT*t*control.x+t*t*end.x,y:oneMinusT*oneMinusT*start.y+2*oneMinusT*t*control.y+t*t*end.y};const tangent={x:2*oneMinusT*(control.x-start.x)+2*t*(end.x-control.x),y:2*oneMinusT*(control.y-start.y)+2*t*(end.y-control.y)};const length=Math.hypot(tangent.x,tangent.y)||1;const offset=edge.labelOffset??-14;return{x:point.x+-tangent.y/length*offset,y:point.y+tangent.x/length*offset}}function edgeSvg(resolved,theme,families){const{edge,start,end,control}=resolved;const toneName=edge.tone??"neutral";const tone=theme.tones[toneName];const marker=edge.arrowhead==="none"?"":edge.arrowhead==="triangle"?`url(#arrow-triangle-${toneName})`:`url(#arrow-open-${toneName})`;const path=`M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`;const labelPoint=resolveEdgeLabel(resolved);const label=edge.label===undefined?"":`<text x="${labelPoint.x}" y="${labelPoint.y}" text-anchor="middle" dominant-baseline="central" fill="${tone.text}" stroke="${theme.background}" stroke-width="6" paint-order="stroke" font-family="${escapeXml(families[edge.labelFontFamily??"default"])}" font-size="${edge.labelFontSize??18}" font-weight="${edge.labelWeight??600}">${escapeXml(edge.label)}</text>`;return`<g data-edge-id="${escapeXml(edge.id)}"><path d="${path}" fill="none" stroke="${tone.stroke}" stroke-width="3" stroke-linecap="round" marker-end="${marker}"/>${label}</g>`}function shapeSvg(shape,theme,families,icons){if(shape.type==="rect"||shape.type==="ellipse"){return boxSvg(shape,theme,families,icons)}if(shape.type==="line"){return`<line data-shape-id="${escapeXml(shape.id)}" x1="${shape.x}" y1="${shape.y}" x2="${shape.x2}" y2="${shape.y2}" stroke="${theme.tones[shape.tone??"neutral"].stroke}" stroke-width="${shape.strokeWidth??3}" stroke-linecap="round" opacity="${shape.opacity??1}"/>`}const text=shape;return textSvg({text:text.text,x:text.x,y:text.y,width:text.width??Math.max(text.text.length*(text.fontSize??24)*0.58,12),fontSize:text.fontSize??24,weight:text.weight??500,align:text.align??"start",color:theme.tones[text.tone??"neutral"].text,opacity:text.opacity??1,family:families[text.fontFamily??"default"]})}async function renderSvg(spec,mode,config){const theme=resolveTheme(mode,config);const font=config.font??defaultFont;const families={default:font.family,mono:font.monoFamily??"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"};const icons=config.icons??{};const embeddedFonts=await fontCss(font);const markerDefinitions=Object.entries(theme.tones).map(([toneName,tone])=>`<marker id="arrow-open-${toneName}" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="strokeWidth"><path d="M2 2 10 6 2 10" fill="none" stroke="${tone.stroke}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></marker><marker id="arrow-triangle-${toneName}" markerWidth="11" markerHeight="11" refX="9" refY="5.5" orient="auto" markerUnits="strokeWidth"><path d="M1 1 10 5.5 1 10z" fill="${tone.stroke}"/></marker>`).join("");const edgeMarkup=(spec.edges??[]).map((edge)=>edgeSvg(resolveEdge(spec,edge),theme,families)).join("");const shapeMarkup=spec.shapes.map((shape)=>shapeSvg(shape,theme,families,icons)).join("");const svg=[`<svg xmlns="http://www.w3.org/2000/svg" width="${spec.canvas.width}" height="${spec.canvas.height}" viewBox="0 0 ${spec.canvas.width} ${spec.canvas.height}" role="img" aria-labelledby="diagram-title" color-scheme="${mode}">`,`<title id="diagram-title">${escapeXml(spec.name)}</title>`,`<style>${embeddedFonts}text{font-kerning:normal;text-rendering:geometricPrecision}</style>`,`<rect width="100%" height="100%" fill="${theme.background}"/>`,`<defs>${markerDefinitions}</defs>`,edgeMarkup,shapeMarkup,"</svg>"].join("");return{mode,svg,width:spec.canvas.width,height:spec.canvas.height}}function renderPng(rendered,config,scale=2){const font=config.font??defaultFont;const fontFiles=font.files?.map((file)=>file.path)??[];const renderer=new Resvg(rendered.svg,{fitTo:{mode:"zoom",value:scale},font:{loadSystemFonts:true,...fontFiles.length===0?{}:{fontFiles},defaultFontFamily:font.family}});return renderer.render().asPng()}function boxOutsideCanvas(shape,spec){return shape.x<0||shape.y<0||shape.x+shape.width>spec.canvas.width||shape.y+shape.height>spec.canvas.height}function lintDiagram(spec){const findings=[];const boxes=spec.shapes.filter((shape)=>shape.type==="rect"||shape.type==="ellipse");for(const shape of boxes){if(boxOutsideCanvas(shape,spec)){findings.push({code:"outside-canvas",message:`${shape.id} extends beyond the canvas`,shapeIds:[shape.id]})}const labels=shape.labelRows?.map((row)=>row.text)??(shape.label===undefined?[]:[shape.label]);const longLabel=labels.find((label)=>label.length>32);if(longLabel!==undefined){findings.push({code:"long-label",message:`${shape.id} has a ${longLabel.length}-character label; prefer a short noun phrase`,shapeIds:[shape.id]})}const iconSize=shape.icon===undefined?undefined:Math.min(shape.iconSize??52,shape.height*0.45,shape.width*0.32);const content=layoutBoxContent(shape,iconSize);const contentTop=content.icon?.y??content.rows[0]?.y;const finalRow=content.rows.at(-1);const contentBottom=finalRow===undefined?content.icon===undefined?undefined:content.icon.y+content.icon.size:finalRow.y+finalRow.height;const contentWidth=Math.max(1,shape.width-32);const exceedsWidth=content.rows.some((row)=>row.lines.some((line)=>estimateDiagramTextWidth(line,row.fontSize,row.fontFamily)>contentWidth));if(contentTop!==undefined&&contentBottom!==undefined&&(contentTop<shape.y||contentBottom>shape.y+shape.height||exceedsWidth)){findings.push({code:"label-overflow",message:`${shape.id} label and icon content exceeds the box bounds; increase the box or reduce authored sizes`,shapeIds:[shape.id]})}if(shape.width<120||shape.height<64){findings.push({code:"small-target",message:`${shape.id} is small enough to make its label or icon hard to scan`,shapeIds:[shape.id]})}}if(boxes.length>9){findings.push({code:"too-many-elements",message:`The diagram has ${boxes.length} primary shapes; consider a higher-level visual`,shapeIds:boxes.map((shape)=>shape.id)})}const sharedTerminals=new Map;for(const edge of spec.edges??[]){const resolved=resolveEdge(spec,edge);for(const terminal of[{kind:"start",point:resolved.start,shapeId:edge.from},{kind:"end",point:resolved.end,shapeId:edge.to}]){const key=`${terminal.kind}:${terminal.shapeId}:${terminal.point.x.toFixed(3)}:${terminal.point.y.toFixed(3)}`;const group=sharedTerminals.get(key)??[];group.push({edgeId:edge.id,shapeId:terminal.shapeId});sharedTerminals.set(key,group)}const length=Math.hypot(resolved.end.x-resolved.start.x,resolved.end.y-resolved.start.y);if(length<96){findings.push({code:"short-arrow",message:`${edge.id} is ${Math.round(length)}px long; leave more space between connected shapes`,shapeIds:[edge.from,edge.to]})}if(edge.label!==undefined&&edge.label.length>24){findings.push({code:"long-edge-label",message:`${edge.id} has a long connector label; prefer one short relation`,shapeIds:[edge.from,edge.to]})}}for(const group of sharedTerminals.values()){if(group.length<2)continue;findings.push({code:"shared-edge-port",message:`${group.map(({edgeId})=>edgeId).join(", ")} share one connector port; set distinct startPosition or endPosition values`,shapeIds:[group[0].shapeId]})}return findings}var diagramVersion=1;var stackLayoutDefaults={gap:160,padding:64,align:"center"};var coordinatePrecision=3;var coordinateScale=10**coordinatePrecision;class StackLayoutError extends Error{issues;constructor(issues){super(`Invalid stack layout:
|
|
3
|
+
`);const maxCharacters=Math.max(1,Math.floor(maxWidth/(fontSize*characterWidthRatio(fontFamily))));const lines=[];for(const explicitLine of explicitLines){if(explicitLine.length<=maxCharacters){lines.push(explicitLine);continue}const words=explicitLine.split(/\s+/);let current="";for(const word of words){const candidate=current===""?word:`${current} ${word}`;if(candidate.length<=maxCharacters||current===""){current=candidate}else{lines.push(current);current=word}}if(current!=="")lines.push(current)}return lines.length===0?[""]:lines}function resolveRows(shape){if(shape.labelRows!==undefined){return shape.labelRows.map((row)=>({text:row.text,fontSize:row.fontSize??defaultLabelFontSize,fontFamily:row.fontFamily??"default",weight:row.weight??defaultLabelWeight,lineHeight:(row.fontSize??defaultLabelFontSize)*defaultLineHeightRatio}))}if(shape.label===undefined)return[];const fontSize=shape.labelFontSize??defaultLabelFontSize;return[{text:shape.label,fontSize,fontFamily:shape.labelFontFamily??"default",weight:shape.labelWeight??defaultLabelWeight,lineHeight:fontSize*defaultLineHeightRatio}]}function layoutBoxContent(shape,resolvedIconSize){const textWidth=Math.max(1,shape.width-32);const pendingRows=resolveRows(shape).map((row)=>{const lines=wrapDiagramText(row.text,textWidth,row.fontSize,row.fontFamily);const height=lines.length*row.lineHeight;return{...row,lines,height}});const rowGap=shape.labelRows===undefined?0:shape.labelRowGap??defaultRowGap;const rowsHeight=pendingRows.reduce((total,row)=>total+row.height,0)+Math.max(0,pendingRows.length-1)*rowGap;const hasIcon=resolvedIconSize!==undefined;const gap=hasIcon&&pendingRows.length>0?iconLabelGap:0;const contentHeight=(resolvedIconSize??0)+gap+rowsHeight;let cursorY=shape.y+(shape.height-contentHeight)/2;const icon=resolvedIconSize===undefined?undefined:{x:shape.x+(shape.width-resolvedIconSize)/2,y:cursorY,size:resolvedIconSize};if(hasIcon)cursorY+=resolvedIconSize+gap;const rows=pendingRows.map((row)=>{const resolved={...row,y:cursorY};cursorY+=row.height+rowGap;return resolved});return{...icon===undefined?{}:{icon},rows}}import{readFile}from"fs/promises";import{extname,isAbsolute}from"path";import{fileURLToPath}from"url";import{Resvg}from"@resvg/resvg-js";var NebulaSans_Bold_default="./NebulaSans-Bold-26se8aek.otf";var NebulaSans_Bold_default2="./NebulaSans-Bold-bcz7y08t.woff2";var NebulaSans_Book_default="./NebulaSans-Book-8cenzchw.otf";var NebulaSans_Book_default2="./NebulaSans-Book-5ax05zvn.woff2";var tones=["neutral","blue","orange","green","red","purple","yellow"];var defaults={light:{background:"#ffffff",foreground:"#18181b",muted:"#71717a",stroke:"#27272a",tones:{neutral:{fill:"#f4f4f5",stroke:"#52525b",text:"#18181b"},blue:{fill:"#dbeafe",stroke:"#2563eb",text:"#1e3a8a"},orange:{fill:"#ffedd5",stroke:"#ea580c",text:"#7c2d12"},green:{fill:"#dcfce7",stroke:"#16a34a",text:"#14532d"},red:{fill:"#fee2e2",stroke:"#dc2626",text:"#7f1d1d"},purple:{fill:"#f3e8ff",stroke:"#9333ea",text:"#581c87"},yellow:{fill:"#fef9c3",stroke:"#ca8a04",text:"#713f12"}}},dark:{background:"#09090b",foreground:"#fafafa",muted:"#a1a1aa",stroke:"#d4d4d8",tones:{neutral:{fill:"#27272a",stroke:"#a1a1aa",text:"#fafafa"},blue:{fill:"#172554",stroke:"#60a5fa",text:"#dbeafe"},orange:{fill:"#431407",stroke:"#fb923c",text:"#ffedd5"},green:{fill:"#052e16",stroke:"#4ade80",text:"#dcfce7"},red:{fill:"#450a0a",stroke:"#f87171",text:"#fee2e2"},purple:{fill:"#3b0764",stroke:"#c084fc",text:"#f3e8ff"},yellow:{fill:"#422006",stroke:"#facc15",text:"#fef9c3"}}}};function mergeTheme(base,override){if(override===undefined)return base;return{background:override.background??base.background,foreground:override.foreground??base.foreground,muted:override.muted??base.muted,stroke:override.stroke??base.stroke,tones:Object.fromEntries(tones.map((tone)=>[tone,{fill:override.tones?.[tone]?.fill??base.tones[tone].fill,stroke:override.tones?.[tone]?.stroke??base.tones[tone].stroke,text:override.tones?.[tone]?.text??base.tones[tone].text}]))}}function resolveTheme(mode,config){return mergeTheme(defaults[mode],config.theme?.[mode])}var bundledAssetPath=(asset)=>isAbsolute(asset)?asset:fileURLToPath(new URL(asset,import.meta.url));var defaultFont=Object.freeze({family:"Nebula Sans",files:Object.freeze([Object.freeze({embed:true,path:bundledAssetPath(NebulaSans_Book_default2),style:"normal",weight:400}),Object.freeze({embed:true,path:bundledAssetPath(NebulaSans_Bold_default2),style:"normal",weight:700}),Object.freeze({embed:false,path:bundledAssetPath(NebulaSans_Book_default),style:"normal",weight:400}),Object.freeze({embed:false,path:bundledAssetPath(NebulaSans_Bold_default),style:"normal",weight:700})])});var escapeXml=(value)=>value.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");function fontMime(filePath){switch(extname(filePath).toLowerCase()){case".otf":return"font/otf";case".woff":return"font/woff";case".woff2":return"font/woff2";default:return"font/ttf"}}async function fontCss(font){if(font.files===undefined)return"";const faces=await Promise.all(font.files.filter((file)=>file.embed===true).map(async(file)=>{const encoded=(await readFile(file.path)).toString("base64");return`@font-face{font-family:"${escapeXml(font.family)}";src:url(data:${fontMime(file.path)};base64,${encoded});font-weight:${file.weight??400};font-style:${file.style??"normal"};}`}));return faces.join("")}function textSvg(options){const lines=options.lines??wrapDiagramText(options.text,options.width,options.fontSize);const lineHeight=options.lineHeight??options.fontSize*1.25;const anchor=options.align;const x=anchor==="middle"?options.x+options.width/2:anchor==="end"?options.x+options.width:options.x;const dominantBaseline=options.centerLineBoxes?"central":"hanging";return`<text x="${x}" y="${options.y}" text-anchor="${anchor}" dominant-baseline="${dominantBaseline}" fill="${options.color}" opacity="${options.opacity}" font-family="${escapeXml(options.family)}" font-size="${options.fontSize}" font-weight="${options.weight}">${lines.map((line,index)=>options.centerLineBoxes?`<tspan x="${x}" y="${options.y+(index+0.5)*lineHeight}">${escapeXml(line)}</tspan>`:`<tspan x="${x}" dy="${index===0?0:lineHeight}">${escapeXml(line)}</tspan>`).join("")}</text>`}function iconSvg(options){const icon=sanitizeIcon(options.icon);const parts=icon.viewBox.trim().split(/\s+/).map(Number);const width=(parts[2]??24)-(parts[0]??0);const height=(parts[3]??24)-(parts[1]??0);const scale=options.size/Math.max(width,height);return`<g color="${options.color}" opacity="${options.opacity}" transform="translate(${options.x} ${options.y}) scale(${scale}) translate(${-Number(parts[0]??0)} ${-Number(parts[1]??0)})">${icon.body}</g>`}function boxSvg(shape,theme,families,icons){const tone=theme.tones[shape.tone??"neutral"];const strokeWidth=shape.strokeWidth??2;const opacity=shape.opacity??1;const radius=shape.type==="ellipse"?0:Math.min(shape.radius??22,shape.height/2);const geometry=shape.type==="ellipse"?`<ellipse cx="${shape.x+shape.width/2}" cy="${shape.y+shape.height/2}" rx="${shape.width/2}" ry="${shape.height/2}" fill="${shape.fill===false?"none":tone.fill}" stroke="${tone.stroke}" stroke-width="${strokeWidth}"/>`:`<rect x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}" rx="${radius}" fill="${shape.fill===false?"none":tone.fill}" stroke="${tone.stroke}" stroke-width="${strokeWidth}"/>`;const icon=shape.icon===undefined?undefined:icons[shape.icon];if(shape.icon!==undefined&&icon===undefined){throw new Error(`Unknown icon "${shape.icon}" on shape ${shape.id}`)}const iconSize=Math.min(shape.iconSize??52,shape.height*0.45,shape.width*0.32);const content=layoutBoxContent(shape,icon===undefined?undefined:iconSize);const iconMarkup=icon===undefined||content.icon===undefined?"":iconSvg({icon,x:content.icon.x,y:content.icon.y,size:content.icon.size,color:tone.text,opacity:1});const labelMarkup=content.rows.map((row)=>textSvg({text:row.text,x:shape.x+16,y:row.y,width:shape.width-32,fontSize:row.fontSize,weight:row.weight,align:"middle",color:tone.text,opacity:1,family:families[row.fontFamily],lineHeight:row.lineHeight,lines:row.lines,centerLineBoxes:true})).join("");return`<g data-shape-id="${escapeXml(shape.id)}" opacity="${opacity}">${geometry}${iconMarkup}${labelMarkup}</g>`}function pointForAnchor(shape,anchor,toward,position){const center={x:shape.x+shape.width/2,y:shape.y+shape.height/2};let resolved=anchor??"auto";if(resolved==="auto"){const dx=toward.x-center.x;const dy=toward.y-center.y;resolved=Math.abs(dx/shape.width)>=Math.abs(dy/shape.height)?dx>=0?"right":"left":dy>=0?"bottom":"top"}const resolvedPosition=position??0.5;if(shape.type==="ellipse"){const projected=(resolvedPosition-0.5)*2;const radial=Math.sqrt(Math.max(0,1-projected*projected))/2;const normalized=resolved==="top"?{x:resolvedPosition,y:0.5-radial}:resolved==="bottom"?{x:resolvedPosition,y:0.5+radial}:resolved==="left"?{x:0.5-radial,y:resolvedPosition}:{x:0.5+radial,y:resolvedPosition};return{x:shape.x+shape.width*normalized.x,y:shape.y+shape.height*normalized.y,normalized}}switch(resolved){case"top":return{x:shape.x+shape.width*resolvedPosition,y:shape.y,normalized:{x:resolvedPosition,y:0}};case"right":return{x:shape.x+shape.width,y:shape.y+shape.height*resolvedPosition,normalized:{x:1,y:resolvedPosition}};case"bottom":return{x:shape.x+shape.width*resolvedPosition,y:shape.y+shape.height,normalized:{x:resolvedPosition,y:1}};case"left":return{x:shape.x,y:shape.y+shape.height*resolvedPosition,normalized:{x:0,y:resolvedPosition}}}}function resolveEdge(spec,edge){const boxes=new Map(spec.shapes.filter((shape)=>shape.type==="rect"||shape.type==="ellipse").map((shape)=>[shape.id,shape]));const from=boxes.get(edge.from);const to=boxes.get(edge.to);if(from===undefined||to===undefined){throw new Error(`Edge ${edge.id} references a missing box`)}const fromCenter={x:from.x+from.width/2,y:from.y+from.height/2};const toCenter={x:to.x+to.width/2,y:to.y+to.height/2};const start=pointForAnchor(from,edge.start,toCenter,edge.startPosition);const end=pointForAnchor(to,edge.end,fromCenter,edge.endPosition);const dx=end.x-start.x;const dy=end.y-start.y;const length=Math.hypot(dx,dy)||1;const bend=edge.bend??0;return{edge,from,to,start,end,control:{x:(start.x+end.x)/2+-dy/length*bend,y:(start.y+end.y)/2+dx/length*bend}}}function resolveEdgeLabel(resolved){const{edge,start,end,control}=resolved;const t=edge.labelPosition??0.5;const oneMinusT=1-t;const point={x:oneMinusT*oneMinusT*start.x+2*oneMinusT*t*control.x+t*t*end.x,y:oneMinusT*oneMinusT*start.y+2*oneMinusT*t*control.y+t*t*end.y};const tangent={x:2*oneMinusT*(control.x-start.x)+2*t*(end.x-control.x),y:2*oneMinusT*(control.y-start.y)+2*t*(end.y-control.y)};const length=Math.hypot(tangent.x,tangent.y)||1;const offset=edge.labelOffset??-14;return{x:point.x+-tangent.y/length*offset,y:point.y+tangent.x/length*offset}}function edgeSvg(resolved,theme,families,strokeWidth){const{edge,start,end,control}=resolved;const toneName=edge.tone??"neutral";const tone=theme.tones[toneName];const marker=edge.arrowhead==="none"?"":edge.arrowhead==="triangle"?`url(#arrow-triangle-${toneName})`:`url(#arrow-open-${toneName})`;const path=`M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`;const labelPoint=resolveEdgeLabel(resolved);const label=edge.label===undefined?"":`<text x="${labelPoint.x}" y="${labelPoint.y}" text-anchor="middle" dominant-baseline="central" fill="${tone.text}" stroke="${theme.background}" stroke-width="6" paint-order="stroke" font-family="${escapeXml(families[edge.labelFontFamily??"default"])}" font-size="${edge.labelFontSize??18}" font-weight="${edge.labelWeight??600}">${escapeXml(edge.label)}</text>`;return`<g data-edge-id="${escapeXml(edge.id)}"><path d="${path}" fill="none" stroke="${tone.stroke}" stroke-width="${strokeWidth}" stroke-linecap="round" marker-end="${marker}"/>${label}</g>`}function shapeSvg(shape,theme,families,icons){if(shape.type==="rect"||shape.type==="ellipse"){return boxSvg(shape,theme,families,icons)}if(shape.type==="line"){return`<line data-shape-id="${escapeXml(shape.id)}" x1="${shape.x}" y1="${shape.y}" x2="${shape.x2}" y2="${shape.y2}" stroke="${theme.tones[shape.tone??"neutral"].stroke}" stroke-width="${shape.strokeWidth??3}" stroke-linecap="round" opacity="${shape.opacity??1}"/>`}const text=shape;return textSvg({text:text.text,x:text.x,y:text.y,width:text.width??Math.max(text.text.length*(text.fontSize??24)*0.58,12),fontSize:text.fontSize??24,weight:text.weight??500,align:text.align??"start",color:theme.tones[text.tone??"neutral"].text,opacity:text.opacity??1,family:families[text.fontFamily??"default"]})}async function renderSvg(spec,mode,config,options={}){const edgeStrokeWidth=options.edgeStrokeWidth??3;if(!Number.isFinite(edgeStrokeWidth)||edgeStrokeWidth<0.5||edgeStrokeWidth>4)throw new Error("Edge stroke width must be between 0.5 and 4");const theme=resolveTheme(mode,config);const font=config.font??defaultFont;const families={default:font.family,mono:font.monoFamily??"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"};const icons=config.icons??{};const embeddedFonts=await fontCss(font);const markerDefinitions=Object.entries(theme.tones).map(([toneName,tone])=>`<marker id="arrow-open-${toneName}" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="strokeWidth"><path d="M2 2 10 6 2 10" fill="none" stroke="${tone.stroke}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></marker><marker id="arrow-triangle-${toneName}" markerWidth="11" markerHeight="11" refX="9" refY="5.5" orient="auto" markerUnits="strokeWidth"><path d="M1 1 10 5.5 1 10z" fill="${tone.stroke}"/></marker>`).join("");const edgeMarkup=(spec.edges??[]).map((edge)=>edgeSvg(resolveEdge(spec,edge),theme,families,edgeStrokeWidth)).join("");const shapeMarkup=spec.shapes.map((shape)=>shapeSvg(shape,theme,families,icons)).join("");const svg=[`<svg xmlns="http://www.w3.org/2000/svg" width="${spec.canvas.width}" height="${spec.canvas.height}" viewBox="0 0 ${spec.canvas.width} ${spec.canvas.height}" role="img" aria-labelledby="diagram-title" color-scheme="${mode}">`,`<title id="diagram-title">${escapeXml(spec.name)}</title>`,`<style>${embeddedFonts}text{font-kerning:normal;text-rendering:geometricPrecision}</style>`,`<rect width="100%" height="100%" fill="${theme.background}"/>`,`<defs>${markerDefinitions}</defs>`,edgeMarkup,shapeMarkup,"</svg>"].join("");return{mode,svg,width:spec.canvas.width,height:spec.canvas.height}}function renderPng(rendered,config,scale=2){const font=config.font??defaultFont;const fontFiles=font.files?.map((file)=>file.path)??[];const renderer=new Resvg(rendered.svg,{fitTo:{mode:"zoom",value:scale},font:{loadSystemFonts:true,...fontFiles.length===0?{}:{fontFiles},defaultFontFamily:font.family}});return renderer.render().asPng()}function boxOutsideCanvas(shape,spec){return shape.x<0||shape.y<0||shape.x+shape.width>spec.canvas.width||shape.y+shape.height>spec.canvas.height}function lintDiagram(spec){const findings=[];const boxes=spec.shapes.filter((shape)=>shape.type==="rect"||shape.type==="ellipse");for(const shape of boxes){if(boxOutsideCanvas(shape,spec)){findings.push({code:"outside-canvas",message:`${shape.id} extends beyond the canvas`,shapeIds:[shape.id]})}const labels=shape.labelRows?.map((row)=>row.text)??(shape.label===undefined?[]:[shape.label]);const longLabel=labels.find((label)=>label.length>32);if(longLabel!==undefined){findings.push({code:"long-label",message:`${shape.id} has a ${longLabel.length}-character label; prefer a short noun phrase`,shapeIds:[shape.id]})}const iconSize=shape.icon===undefined?undefined:Math.min(shape.iconSize??52,shape.height*0.45,shape.width*0.32);const content=layoutBoxContent(shape,iconSize);const contentTop=content.icon?.y??content.rows[0]?.y;const finalRow=content.rows.at(-1);const contentBottom=finalRow===undefined?content.icon===undefined?undefined:content.icon.y+content.icon.size:finalRow.y+finalRow.height;const contentWidth=Math.max(1,shape.width-32);const exceedsWidth=content.rows.some((row)=>row.lines.some((line)=>estimateDiagramTextWidth(line,row.fontSize,row.fontFamily)>contentWidth));if(contentTop!==undefined&&contentBottom!==undefined&&(contentTop<shape.y||contentBottom>shape.y+shape.height||exceedsWidth)){findings.push({code:"label-overflow",message:`${shape.id} label and icon content exceeds the box bounds; increase the box or reduce authored sizes`,shapeIds:[shape.id]})}if(shape.width<120||shape.height<64){findings.push({code:"small-target",message:`${shape.id} is small enough to make its label or icon hard to scan`,shapeIds:[shape.id]})}}if(boxes.length>9){findings.push({code:"too-many-elements",message:`The diagram has ${boxes.length} primary shapes; consider a higher-level visual`,shapeIds:boxes.map((shape)=>shape.id)})}const sharedTerminals=new Map;for(const edge of spec.edges??[]){const resolved=resolveEdge(spec,edge);for(const terminal of[{kind:"start",point:resolved.start,shapeId:edge.from},{kind:"end",point:resolved.end,shapeId:edge.to}]){const key=`${terminal.kind}:${terminal.shapeId}:${terminal.point.x.toFixed(3)}:${terminal.point.y.toFixed(3)}`;const group=sharedTerminals.get(key)??[];group.push({edgeId:edge.id,shapeId:terminal.shapeId});sharedTerminals.set(key,group)}const length=Math.hypot(resolved.end.x-resolved.start.x,resolved.end.y-resolved.start.y);if(length<96){findings.push({code:"short-arrow",message:`${edge.id} is ${Math.round(length)}px long; leave more space between connected shapes`,shapeIds:[edge.from,edge.to]})}if(edge.label!==undefined&&edge.label.length>24){findings.push({code:"long-edge-label",message:`${edge.id} has a long connector label; prefer one short relation`,shapeIds:[edge.from,edge.to]})}}for(const group of sharedTerminals.values()){if(group.length<2)continue;findings.push({code:"shared-edge-port",message:`${group.map(({edgeId})=>edgeId).join(", ")} share one connector port; set distinct startPosition or endPosition values`,shapeIds:[group[0].shapeId]})}return findings}var diagramVersion=1;var stackLayoutDefaults={gap:160,padding:64,align:"center"};var coordinatePrecision=3;var coordinateScale=10**coordinatePrecision;class StackLayoutError extends Error{issues;constructor(issues){super(`Invalid stack layout:
|
|
4
4
|
${issues.map((issue)=>`- ${issue}`).join(`
|
|
5
5
|
`)}`);this.name="StackLayoutError";this.issues=issues}}function coordinateFromUnits(value){return value/coordinateScale}function normalizedScaledCoordinate(value){const scaled=value*coordinateScale;return normalizedGridValue(scaled)}function normalizedGridValue(value){const nearest=Math.round(value);return Math.abs(value-nearest)<=0.000000001?nearest:value}function floorGridValue(value){return Math.floor(normalizedGridValue(value))}function ceilGridValue(value){return Math.ceil(normalizedGridValue(value))}function coordinateUnits(value,label,positive,rounding,issues){if(!Number.isFinite(value)||(positive?value<=0:value<0)){issues.push(positive?`${label} must be positive`:`${label} must not be negative`);return 0}const scaled=normalizedScaledCoordinate(value);const units=rounding==="ceil"?ceilGridValue(scaled):floorGridValue(scaled);if(!Number.isSafeInteger(units)){issues.push(`${label} must be a finite coordinate within the supported range`);return 0}if(positive&&units<1){issues.push(`${label} must occupy at least ${coordinateFromUnits(1)}px in the layout grid`);return 0}return units}function positionedShape(shape,mainPosition,crossPosition,horizontal){return{...shape,x:coordinateFromUnits(horizontal?mainPosition:crossPosition),y:coordinateFromUnits(horizontal?crossPosition:mainPosition)}}function authoredMainSize(shape,horizontal){return horizontal?shape.width:shape.height}function emittedMainPlacement(shapes,horizontal,gap,issues){if(shapes.length===0)return{offsets:[],span:0};const offsets=[];let offset=0;for(const[index,shape]of shapes.entries()){offsets.push(offset);if(index===shapes.length-1)break;const advance=coordinateUnits(authoredMainSize(shape,horizontal)+gap,`space after shape ${shape.id}`,true,"ceil",issues);offset+=advance;if(!Number.isSafeInteger(offset)){issues.push("stack positions exceed the supported coordinate range");return{offsets,span:Number.POSITIVE_INFINITY}}}const last=shapes.at(-1);const span=offset+(last===undefined?0:normalizedScaledCoordinate(authoredMainSize(last,horizontal)));if(!Number.isFinite(span)||!Number.isSafeInteger(Math.ceil(span))){issues.push("stack extent exceeds the supported coordinate range")}return{offsets,span}}function clampedGridPosition(ideal,minimum,maximum){return Math.min(maximum,Math.max(minimum,Math.round(ideal)))}function resolvedAnchors(fromIndex,toIndex,horizontal){const forward=fromIndex<toIndex;if(horizontal){return forward?{start:"right",end:"left"}:{start:"left",end:"right"}}return forward?{start:"bottom",end:"top"}:{start:"top",end:"bottom"}}function resolvedEdge(edge,indexes,horizontal){const fromIndex=indexes.get(edge.from);const toIndex=indexes.get(edge.to);if(fromIndex===undefined||toIndex===undefined){throw new StackLayoutError([`edge ${edge.id} must reference shapes that belong to the stack`])}const anchors=resolvedAnchors(fromIndex,toIndex,horizontal);return{...edge,start:anchors.start,end:anchors.end,bend:0}}function stackStructureIssues(source){const issues=[];if(source.shapes.length<1||source.shapes.length>9){issues.push("stack layouts must contain between 1 and 9 shapes")}const indexes=new Map;const recordIds=new Set;for(const[index,shape]of source.shapes.entries()){if(recordIds.has(shape.id)){issues.push(`shape id ${shape.id} is duplicated`)}else{indexes.set(shape.id,index);recordIds.add(shape.id)}}const connectedPairs=new Set;for(const stackEdge of source.edges??[]){const edge=stackEdge;if(recordIds.has(edge.id)){issues.push(`edge id ${edge.id} is duplicated`)}recordIds.add(edge.id);const fromIndex=indexes.get(edge.from);const toIndex=indexes.get(edge.to);if(fromIndex===undefined){issues.push(`edge ${edge.id} has unknown from id ${edge.from}`)}if(toIndex===undefined){issues.push(`edge ${edge.id} has unknown to id ${edge.to}`)}if(fromIndex!==undefined&&toIndex!==undefined){if(fromIndex===toIndex){issues.push(`edge ${edge.id} cannot connect a shape to itself`)}else{if(Math.abs(fromIndex-toIndex)!==1){issues.push(`edge ${edge.id} must connect adjacent stack shapes`)}const pair=[fromIndex,toIndex].sort((left,right)=>left-right).join(":");if(connectedPairs.has(pair)){issues.push(`edge ${edge.id} duplicates a connection between the same stack shapes`)}connectedPairs.add(pair)}}if(edge.start!==undefined&&edge.start!=="auto"){issues.push(`edge ${edge.id}.start must be auto or omitted in a stack layout`)}if(edge.end!==undefined&&edge.end!=="auto"){issues.push(`edge ${edge.id}.end must be auto or omitted in a stack layout`)}if(edge.bend!==undefined&&edge.bend!==0){issues.push(`edge ${edge.id}.bend must be 0 or omitted in a stack layout`)}}return issues}function resolveStackLayout(source){const issues=[...stackStructureIssues(source)];const horizontal=source.layout.direction==="horizontal";coordinateUnits(source.canvas.width,"canvas width",true,"floor",issues);coordinateUnits(source.canvas.height,"canvas height",true,"floor",issues);const gapValue=source.layout.gap??stackLayoutDefaults.gap;coordinateUnits(gapValue,"stack gap",false,"ceil",issues);const paddingValue=source.canvas.padding??stackLayoutDefaults.padding;const padding=coordinateUnits(paddingValue,"canvas padding",false,"ceil",issues);for(const shape of source.shapes){coordinateUnits(shape.width,`shape ${shape.id} width`,true,"ceil",issues);coordinateUnits(shape.height,`shape ${shape.id} height`,true,"ceil",issues)}const align=source.layout.align??stackLayoutDefaults.align;const canvasMainValue=horizontal?source.canvas.width:source.canvas.height;const canvasCrossValue=horizontal?source.canvas.height:source.canvas.width;const mainPlacement=emittedMainPlacement(source.shapes,horizontal,gapValue,issues);const maximumMainStart=floorGridValue(normalizedScaledCoordinate(canvasMainValue-paddingValue)-mainPlacement.span);if(padding>maximumMainStart){const requiredMain=ceilGridValue(mainPlacement.span);const availableMain=floorGridValue(normalizedScaledCoordinate(canvasMainValue-paddingValue*2));issues.push(`${source.layout.direction} stack needs ${coordinateFromUnits(requiredMain)}px but only ${coordinateFromUnits(availableMain)}px remain inside ${paddingValue}px padding`)}const crossStarts=source.shapes.map((shape)=>{const authoredSize=horizontal?shape.height:shape.width;const maximum=floorGridValue(normalizedScaledCoordinate(canvasCrossValue-paddingValue-authoredSize));if(padding>maximum){const requiredCross=ceilGridValue(normalizedScaledCoordinate(authoredSize));const availableCross=floorGridValue(normalizedScaledCoordinate(canvasCrossValue-paddingValue*2));issues.push(`shape ${shape.id} needs ${coordinateFromUnits(requiredCross)}px on the cross axis but only ${coordinateFromUnits(availableCross)}px remain inside ${paddingValue}px padding`)}return{authoredSize,maximum}});if(issues.length>0)throw new StackLayoutError(issues);const mainStart=clampedGridPosition((normalizedScaledCoordinate(canvasMainValue)-mainPlacement.span)/2,padding,maximumMainStart);const shapes=source.shapes.map((shape,index)=>{const crossStart=crossStarts[index];if(crossStart===undefined){throw new StackLayoutError([`shape ${shape.id} has no cross-axis placement`])}const crossPosition=align==="start"?padding:align==="end"?crossStart.maximum:clampedGridPosition(normalizedScaledCoordinate((canvasCrossValue-crossStart.authoredSize)/2),padding,crossStart.maximum);const mainPosition=mainStart+(mainPlacement.offsets[index]??0);const positioned=positionedShape(shape,mainPosition,crossPosition,horizontal);return positioned});const indexes=new Map(shapes.map((shape,index)=>[shape.id,index]));const edges=source.edges?.map((edge)=>resolvedEdge(edge,indexes,horizontal));return{...source.$schema===undefined?{}:{$schema:source.$schema},version:source.version,name:source.name,canvas:{width:source.canvas.width,height:source.canvas.height,padding:paddingValue},shapes,...edges===undefined?{}:{edges}}}function resolveDiagramSource(source){return"layout"in source?resolveStackLayout(source):source}var tones2=new Set(["neutral","blue","orange","green","red","purple","yellow"]);var anchors=new Set(["auto","top","right","bottom","left"]);var fontFamilies=new Set(["default","mono"]);var stackDirections=new Set(["horizontal","vertical"]);var stackAlignments=new Set(["start","center","end"]);var idPattern=/^[A-Za-z0-9][A-Za-z0-9_-]*$/;var namePattern=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;class DiagramValidationError extends Error{issues;constructor(issues){super(`Invalid diagram specification:
|
|
6
6
|
${issues.map((issue)=>`- ${issue}`).join(`
|
|
@@ -32,4 +32,4 @@ Ink: ${input.ink}
|
|
|
32
32
|
|
|
33
33
|
`)}function axisSpec(value){if(typeof value==="string"){const axis2=value.trim();if(!slopcameraGalleryAxes.includes(axis2)){invalidArgument2(`vary axis must be one of: ${slopcameraGalleryAxes.join(", ")}.`)}return{axis:axis2}}if(!isRecord3(value))invalidArgument2("vary entries must be axis names or {axis, values} objects.");const axis=value.axis;if(!slopcameraGalleryAxes.includes(axis)){invalidArgument2(`vary axis must be one of: ${slopcameraGalleryAxes.join(", ")}.`)}const values=value.values;if(values!==undefined){if(!Array.isArray(values)||values.length<1||values.length>slopcameraGalleryLimits.candidates){invalidArgument2("vary values must be a bounded non-empty list.")}return{axis,values:values.map((item)=>boundedText2(item,"vary value",slopcameraGalleryLimits.variantBytes))}}return{axis}}function explicitCandidate(value){if(!isRecord3(value))invalidArgument2("candidates entries must be {id, prompt|variant} objects.");const id=galleryCandidateId(value.id);const prompt=value.prompt;const variant=value.variant;if(prompt!==undefined&&variant!==undefined){invalidArgument2("A candidate supplies prompt or variant, not both.")}if(prompt===undefined&&variant===undefined){invalidArgument2("A candidate requires a prompt or a variant.")}return{id,...prompt===undefined?{}:{prompt:boundedText2(prompt,"candidate prompt",slopcameraGalleryLimits.candidatePromptBytes)},...variant===undefined?{}:{variant:boundedText2(variant,"candidate variant",slopcameraGalleryLimits.variantBytes)}}}function planSlopcameraGallery(input){if(!isRecord3(input))invalidArgument2("Gallery input must be an object.");const subject=boundedText2(input.subject,"subject",slopcameraGalleryLimits.subjectBytes);const kind=galleryKind(input.kind);const aspect=GALLERY_KIND_CONTRACTS[kind].aspect;const vary=(input.vary??[]).map(axisSpec);const explicit=input.candidates?.map(explicitCandidate);if(explicit!==undefined&&vary.length>0){invalidArgument2("candidates and vary are mutually exclusive.")}if(explicit!==undefined&&input.count!==undefined){invalidArgument2("candidates and count are mutually exclusive.")}if(vary.length>slopcameraGalleryAxes.length||new Set(vary.map((spec)=>spec.axis)).size!==vary.length){invalidArgument2("vary axes must be unique.")}const count=input.count;if(count!==undefined&&(!Number.isInteger(count)||count<1||count>slopcameraGalleryLimits.candidates)){invalidArgument2(`count must be an integer from 1 through ${slopcameraGalleryLimits.candidates}.`)}const tiled=input.tiled;if(tiled!==undefined&&typeof tiled!=="boolean"){invalidArgument2("tiled must be a boolean when set.")}const axes=vary.map((spec)=>({axis:spec.axis,values:spec.values??axisValues(kind,spec.axis).slice(0,slopcameraGalleryLimits.candidates)}));const prompts=[];if(explicit!==undefined){if(explicit.length<1||explicit.length>slopcameraGalleryLimits.candidates){invalidArgument2(`candidates must contain 1 through ${slopcameraGalleryLimits.candidates} entries.`)}for(const candidate of explicit){prompts.push({id:candidate.id,prompt:candidate.prompt??galleryPromptFor(subject,kind,candidate.variant),label:candidate.id})}}else if(axes.length>0){const total=axes.reduce((product,spec)=>product*spec.values.length,1);if(total<1||total>slopcameraGalleryLimits.candidates){invalidArgument2(`vary expands to ${String(total)} candidates; the bound is ${slopcameraGalleryLimits.candidates}.`)}const seen=new Set;for(let combination=0;combination<total;combination++){let remainder=combination;const parts=[];for(let index=axes.length-1;index>=0;index--){const spec=axes[index];const value=spec.values[remainder%spec.values.length];remainder=Math.floor(remainder/spec.values.length);parts.unshift(`${spec.axis}: ${value}`)}const direction=parts.join("; ");let id=slug(parts.map((part)=>part.split(": ")[1]).join("-"));if(seen.has(id))id=`${id}-${String(combination+1)}`;seen.add(id);prompts.push({id,prompt:galleryPromptFor(subject,kind,direction),label:id})}}else{const requested=count??4;for(let index=0;index<requested;index++){const id=`candidate-${String(index+1)}`;prompts.push({id,prompt:galleryPromptFor(subject,kind),label:id})}}return{subject,kind,aspect,axes,candidates:prompts.map((candidate,index)=>({index:index+1,...candidate})),tiled:tiled??kind==="texture"}}function parseSlopcameraGalleryVary(value){const specs=[];for(const part of value.split(";")){const trimmed=part.trim();if(trimmed==="")continue;const[axis,values]=trimmed.split("=",2);if(!slopcameraGalleryAxes.includes(axis.trim())){invalidArgument2(`vary axis must be one of: ${slopcameraGalleryAxes.join(", ")}.`)}if(values===undefined){specs.push({axis:axis.trim()});continue}const list=values.split(",").map((item)=>item.trim()).filter((item)=>item.length>0);if(list.length===0||list.length>slopcameraGalleryLimits.candidates){invalidArgument2("vary values must be a non-empty bounded comma-separated list.")}specs.push({axis:axis.trim(),values:list})}if(specs.length===0){invalidArgument2("vary requires at least one axis, e.g. 'style; palette=warm,cool'.")}return specs}var FONT_5X7={" ":["00000","00000","00000","00000","00000","00000","00000"],"0":["01110","10001","10011","10101","11001","10001","01110"],"1":["00100","01100","00100","00100","00100","00100","01110"],"2":["01110","10001","00001","00010","00100","01000","11111"],"3":["11111","00010","00100","00010","00001","10001","01110"],"4":["00010","00110","01010","11111","00010","00010","00010"],"5":["11111","10000","11110","00001","00001","10001","01110"],"6":["00110","01000","10000","11110","10001","10001","01110"],"7":["11111","00001","00010","00100","01000","01000","01000"],"8":["01110","10001","10001","01110","10001","10001","01110"],"9":["01110","10001","10001","01111","00001","00010","01100"],a:["00000","00000","01110","00001","01111","10001","01111"],b:["10000","10000","10110","11001","10001","10001","11110"],c:["00000","00000","01110","10000","10000","10001","01110"],d:["00001","00001","01101","10011","10001","10001","01111"],e:["00000","00000","01110","10001","11111","10000","01110"],f:["00110","01001","01000","11100","01000","01000","01000"],g:["00000","01111","10001","10001","01111","00001","01110"],h:["10000","10000","10110","11001","10001","10001","10001"],i:["00100","00000","01100","00100","00100","00100","01110"],j:["00010","00000","00110","00010","00010","10010","01100"],k:["10000","10000","10010","10100","11000","10100","10010"],l:["01100","00100","00100","00100","00100","00100","01110"],m:["00000","00000","11010","10101","10101","10101","10101"],n:["00000","00000","10110","11001","10001","10001","10001"],o:["00000","00000","01110","10001","10001","10001","01110"],p:["00000","00000","11110","10001","11110","10000","10000"],q:["00000","00000","01101","10011","01111","00001","00001"],r:["00000","00000","10110","11001","10000","10000","10000"],s:["00000","00000","01111","10000","01110","00001","11110"],t:["01000","01000","11100","01000","01000","01001","00110"],u:["00000","00000","10001","10001","10001","10011","01101"],v:["00000","00000","10001","10001","10001","01010","00100"],w:["00000","00000","10001","10101","10101","10101","01010"],x:["00000","00000","10001","01010","00100","01010","10001"],y:["00000","00000","10001","10001","01111","00001","01110"],z:["00000","00000","11111","00010","00100","01000","11111"],"#":["01010","01010","11111","01010","11111","01010","01010"],"-":["00000","00000","00000","11111","00000","00000","00000"],_:["00000","00000","00000","00000","00000","00000","11111"],".":["00000","00000","00000","00000","00000","01100","01100"],":":["00000","01100","01100","00000","01100","01100","00000"],"=":["00000","00000","11111","00000","11111","00000","00000"],"/":["00001","00010","00010","00100","01000","01000","10000"],"?":["01110","10001","00001","00010","00100","00000","00100"]};function labelPixels(text,width,height){const scale=2;const rgba=Buffer.alloc(width*height*4);for(let pixel=0;pixel<width*height;pixel++){const index=pixel*4;rgba[index]=18;rgba[index+1]=21;rgba[index+2]=27;rgba[index+3]=255}const draw=(character,originX)=>{const glyph=FONT_5X7[character]??FONT_5X7["?"];const originY=Math.floor((height-7*scale)/2);for(let row=0;row<7;row++){for(let column=0;column<5;column++){if(glyph[row][column]!=="1")continue;for(let dy=0;dy<scale;dy++){for(let dx=0;dx<scale;dx++){const x=originX+column*scale+dx;const y=originY+row*scale+dy;if(x<0||x>=width||y<0||y>=height)continue;const index=(y*width+x)*4;rgba[index]=235;rgba[index+1]=238;rgba[index+2]=244;rgba[index+3]=255}}}}};const advance=6*scale;const maximum=Math.floor((width-8)/advance);const sanitized=text.toLowerCase().replace(/[^a-z0-9 #._:=/?-]/gu,"?").slice(0,Math.max(0,maximum));for(let index=0;index<sanitized.length;index++){draw(sanitized[index],4+index*advance)}return rgba}function mediaExtension(mediaType){if(mediaType==="image/png")return"png";if(mediaType==="image/jpeg")return"jpg";if(mediaType==="image/webp")return"webp";invalidArgument2("Candidate media type is unsupported.");return"png"}async function atomicPublish(path,bytes){const temporaryPath=join(dirname2(path),`.${randomUUID2()}.slopcamera-gallery.tmp`);try{await writeFile2(temporaryPath,bytes,{flag:"wx"});await link(temporaryPath,path)}catch{throw new SlopcameraCloudError("OUTPUT_WRITE_FAILED","Slopcamera could not atomically write a gallery output; existing files are never replaced.")}finally{await rm2(temporaryPath,{force:true}).catch(()=>{return})}}async function generateCandidate(candidate,model,input,dependencies){const generate=dependencies.generate??generateSlopcameraImage;try{const generated=await generate({model,prompt:candidate.prompt,...input.signal===undefined?{}:{signal:input.signal},...input.timeoutMs===undefined?{}:{timeoutMs:input.timeoutMs}},dependencies);const bytes=Buffer.from(generated.image.base64,"base64");return{index:candidate.index,id:candidate.id,label:candidate.label,prompt:candidate.prompt,status:"generated",bytes,mediaType:generated.image.mediaType,requestId:generated.requestId,warnings:generated.warnings}}catch(error){return{index:candidate.index,id:candidate.id,label:candidate.label,prompt:candidate.prompt,status:"failed",error:error instanceof Error?error.message:String(error)}}}async function mapBounded(items,concurrency,worker){const results=new Array(items.length);let next=0;await Promise.all(Array.from({length:Math.min(concurrency,items.length)},async()=>{while(next<items.length){const index=next++;results[index]=await worker(items[index])}}));return results}function galleryModel(value){const model=value??slopcameraImageModels[1];if(typeof model!=="string"||model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){invalidArgument2("model must be a bounded Vercel AI Gateway provider/model id.")}return model}function galleryCellEdge(value){const cellEdge=value??slopcameraGalleryLimits.cellEdgeDefault;if(typeof cellEdge!=="number"||!Number.isInteger(cellEdge)||cellEdge<slopcameraGalleryLimits.cellEdgeMin||cellEdge>slopcameraGalleryLimits.cellEdgeMax){invalidArgument2(`cellEdge must be an integer from ${String(slopcameraGalleryLimits.cellEdgeMin)} through ${String(slopcameraGalleryLimits.cellEdgeMax)}.`)}return cellEdge}function galleryOutputDir(value){if(typeof value!=="string"||value.length<1||value.length>4096||value.includes("\x00")){invalidArgument2("outputDir must be a non-empty bounded local directory path.")}return resolve2(value)}async function generateSlopcameraImageGallery(rawInput,dependencies={}){if(!isRecord3(rawInput))invalidArgument2("Gallery input must be an object.");const input=rawInput;const outputDir=galleryOutputDir(input.outputDir);const model=galleryModel(input.model);const cellEdge=galleryCellEdge(input.cellEdge);const plan=planSlopcameraGallery(input);await mkdir2(outputDir,{recursive:true});const generated=await mapBounded(plan.candidates,slopcameraGalleryLimits.concurrency,async(candidate)=>await generateCandidate(candidate,model,input,dependencies));return await composeSlopcameraImageGallery({candidates:generated,cellEdge,model,outputDir,plan})}async function composeSlopcameraImageGallery(input){const outputDir=galleryOutputDir(input.outputDir);const model=galleryModel(input.model);const cellEdge=galleryCellEdge(input.cellEdge);const{plan}=input;if(!isRecord3(plan)||!Array.isArray(plan.candidates)||plan.candidates.length===0){invalidArgument2("plan must be a non-empty gallery plan from planSlopcameraGallery.")}const generated=input.candidates;await mkdir2(outputDir,{recursive:true});const labelHeight=slopcameraGalleryLimits.labelHeight;const cellWidth=cellEdge;const cellHeight=Math.round(cellEdge*plan.aspect[1]/plan.aspect[0]);const cellBodyHeight=cellHeight+labelHeight;const columns=Math.max(1,Math.ceil(Math.sqrt(generated.length)));const rows=Math.ceil(generated.length/columns);const gap=slopcameraGalleryLimits.cellGap;const sheetWidth=columns*cellWidth+(columns-1)*gap;const sheetHeight=rows*cellBodyHeight+(rows-1)*gap;if(sheetWidth*sheetHeight>slopcameraGalleryLimits.galleryPixels){invalidArgument2("Gallery sheet exceeds its pixel budget; reduce count or cellEdge.")}const receiptCandidates=[];const overlays=[];for(const candidate of generated){const column=(candidate.index-1)%columns;const row=Math.floor((candidate.index-1)/columns);const x=column*(cellWidth+gap);const y=row*(cellBodyHeight+gap);const labelText=candidate.status==="failed"?`#${String(candidate.index)} ${candidate.label} failed`:`#${String(candidate.index)} ${candidate.label}`;overlays.push({input:await sharp(labelPixels(labelText,cellWidth,labelHeight),{raw:{width:cellWidth,height:labelHeight,channels:4}}).png().toBuffer(),left:x,top:y});if(candidate.status==="generated"){if(candidate.bytes===undefined||candidate.mediaType===undefined){invalidArgument2("A generated gallery candidate requires its bytes and media type.")}const path=candidate.path??join(outputDir,`candidate-${String(candidate.index).padStart(2,"0")}-${slug(candidate.id)}.${mediaExtension(candidate.mediaType)}`);if(candidate.path===undefined){await atomicPublish(path,candidate.bytes)}const sha256=createHash2("sha256").update(candidate.bytes).digest("hex");if(candidate.sha256!==undefined&&candidate.sha256!==sha256){invalidArgument2("A pre-published gallery candidate digest does not match its bytes.")}let cellSource;let cellReceipt;if(candidate.cellImage!==undefined){const cellSha256=createHash2("sha256").update(candidate.cellImage.bytes).digest("hex");if(candidate.cellImage.sha256!==undefined&&candidate.cellImage.sha256!==cellSha256){invalidArgument2("A gallery cell image digest does not match its bytes.")}const cellCandidate=sharp(candidate.cellImage.bytes,{limitInputPixels:slopcameraGalleryLimits.candidatePixels,failOn:"warning"});const cellMetadata=await cellCandidate.metadata();if(cellMetadata.width===undefined||cellMetadata.height===undefined||cellMetadata.width>slopcameraGalleryLimits.candidateEdge||cellMetadata.height>slopcameraGalleryLimits.candidateEdge||cellMetadata.width*cellMetadata.height>slopcameraGalleryLimits.candidatePixels){invalidArgument2("A gallery cell image exceeds its raster bounds.")}cellSource=cellCandidate;cellReceipt={sha256:cellSha256,bytes:candidate.cellImage.bytes.byteLength,mediaType:candidate.cellImage.mediaType,...candidate.cellImage.path===undefined?{}:{path:candidate.cellImage.path}}}else{const source=sharp(candidate.bytes,{limitInputPixels:slopcameraGalleryLimits.candidatePixels,failOn:"warning"});const metadata=await source.metadata();if(metadata.width===undefined||metadata.height===undefined||metadata.width>slopcameraGalleryLimits.candidateEdge||metadata.height>slopcameraGalleryLimits.candidateEdge||metadata.width*metadata.height>slopcameraGalleryLimits.candidatePixels){invalidArgument2("A generated candidate exceeds its raster bounds.")}cellSource=source}const resized=await cellSource.resize(cellWidth,cellHeight,{fit:"cover"}).png().toBuffer();const cellImage=plan.tiled&&candidate.cellImage===undefined?await(async()=>{const tile=await sharp(resized).resize(Math.ceil(cellWidth/2),Math.ceil(cellHeight/2),{fit:"fill"}).png().toBuffer();const positions=[0,Math.floor(cellWidth/2)];const rows2=[0,Math.floor(cellHeight/2)];return sharp({create:{width:cellWidth,height:cellHeight,channels:4,background:{r:0,g:0,b:0,alpha:0}}}).composite(rows2.flatMap((top)=>positions.map((left)=>({input:tile,left,top})))).png().toBuffer()})():resized;overlays.push({input:cellImage,left:x,top:y+labelHeight});receiptCandidates.push({index:candidate.index,id:candidate.id,label:candidate.label,prompt:candidate.prompt,status:"generated",path,sha256,bytes:candidate.bytes.byteLength,mediaType:candidate.mediaType,...candidate.requestId===undefined?{}:{requestId:candidate.requestId},...candidate.warnings===undefined?{}:{warnings:candidate.warnings},...candidate.job===undefined?{}:{job:candidate.job},...plan.tiled&&candidate.cellImage===undefined?{tiled:true}:{},...cellReceipt===undefined?{}:{cellImage:cellReceipt},...candidate.scene===undefined?{}:{scene:candidate.scene},cell:{x,y,width:cellWidth,height:cellBodyHeight}})}else{const hasRetainedArtifact=candidate.bytes!==undefined||candidate.mediaType!==undefined||candidate.path!==undefined||candidate.sha256!==undefined;let retainedArtifact;if(hasRetainedArtifact){if(candidate.bytes===undefined||candidate.mediaType===undefined||candidate.path===undefined){invalidArgument2("A failed gallery candidate artifact requires bytes, media type, and path.")}const sha256=createHash2("sha256").update(candidate.bytes).digest("hex");if(candidate.sha256!==undefined&&candidate.sha256!==sha256){invalidArgument2("A failed gallery candidate artifact digest does not match its bytes.")}retainedArtifact={bytes:candidate.bytes.byteLength,mediaType:candidate.mediaType,path:candidate.path,sha256}}const blank=Buffer.alloc(cellWidth*cellHeight*4);for(let pixel=0;pixel<cellWidth*cellHeight;pixel++){const index=pixel*4;blank[index]=48;blank[index+1]=52;blank[index+2]=60;blank[index+3]=255}overlays.push({input:await sharp(blank,{raw:{width:cellWidth,height:cellHeight,channels:4}}).png().toBuffer(),left:x,top:y+labelHeight});receiptCandidates.push({index:candidate.index,id:candidate.id,label:candidate.label,prompt:candidate.prompt,status:"failed",...retainedArtifact,...candidate.requestId===undefined?{}:{requestId:candidate.requestId},...candidate.warnings===undefined?{}:{warnings:candidate.warnings},...candidate.error===undefined?{}:{error:candidate.error},...candidate.job===undefined?{}:{job:candidate.job},...candidate.scene===undefined?{}:{scene:candidate.scene},cell:{x,y,width:cellWidth,height:cellBodyHeight}})}}const sheet=await sharp({create:{width:sheetWidth,height:sheetHeight,channels:4,background:{r:10,g:12,b:16,alpha:255}}}).composite(overlays).png().toBuffer();const galleryPath=join(outputDir,"gallery.png");await atomicPublish(galleryPath,sheet);const gallerySha256=createHash2("sha256").update(sheet).digest("hex");const failed=generated.filter((candidate)=>candidate.status==="failed").length;const receiptPath=join(outputDir,"receipt.json");const receipt={kind:input.receipt?.kind??"slopcamera.image-gallery",schemaVersion:1,subject:plan.subject,galleryKind:input.receipt===undefined?plan.kind:"scene",model,provider:input.receipt===undefined?"vercel-ai-gateway":"local",...input.receipt===undefined?{}:{baseSceneSha256:input.receipt.baseSceneSha256},cell:{width:cellWidth,height:cellHeight,labelHeight},axes:plan.axes,candidates:receiptCandidates,counts:{requested:generated.length,generated:generated.length-failed,failed},gallery:{path:galleryPath,sha256:gallerySha256,bytes:sheet.byteLength,mediaType:"image/png",width:sheetWidth,height:sheetHeight},receiptPath,clientMaxRetries:0};await atomicPublish(receiptPath,`${JSON.stringify(receipt,null,2)}
|
|
34
34
|
`);if(failed===generated.length){throw new SlopcameraCloudError("GENERATION_FAILED",`Every gallery candidate failed; see ${receiptPath} for the retained attempt record.`)}return receipt}var slopcameraOperationCodes=["slopcamera.diagram.check","slopcamera.diagram.render","slopcamera.image.vectorize","slopcamera.image.generate","slopcamera.image.icon","slopcamera.image.gallery"];class SlopcameraOperationError extends Error{code;constructor(code,message){super(`[${code}] ${message}`);this.name="SlopcameraOperationError";this.code=code}}var modelSchema={type:"string",minLength:3,maxLength:256,pattern:"^[a-zA-Z0-9][a-zA-Z0-9._-]*/[a-zA-Z0-9][a-zA-Z0-9._:-]*$"};var pathSchema={type:"string",minLength:1,maxLength:4096};function deepFreeze(value){if(typeof value!=="object"||value===null||Object.isFrozen(value)){return value}for(const nested of Object.values(value))deepFreeze(nested);return Object.freeze(value)}var slopcameraOperationRegistry=deepFreeze([{code:"slopcamera.diagram.check",title:"Check diagram",description:"Parse and lint a checked Slopcamera diagram source without changing its files.",execution:"local",authentication:"none",destructive:false,idempotent:true,inputSchema:{type:"object",additionalProperties:false,required:["path"],properties:{path:pathSchema}},resources:[{resource:"cpu",amount:1},{resource:"local-io",amount:1}]},{code:"slopcamera.diagram.render",title:"Render diagram",description:"Render a checked Slopcamera diagram source to its replaceable light, dark, PNG, SVG, and tldraw artifacts.",execution:"local",authentication:"none",destructive:true,idempotent:true,inputSchema:{type:"object",additionalProperties:false,required:["path"],properties:{path:pathSchema,outDirectory:pathSchema,scale:{type:"number",exclusiveMinimum:0,maximum:4}}},resources:[{resource:"cpu",amount:1},{resource:"local-io",amount:1}]},{code:"slopcamera.image.vectorize",title:"Vectorize image",description:"Convert a local caller-owned raster into a bounded inert SVG without authentication or network access.",execution:"local",authentication:"none",destructive:true,idempotent:false,inputSchema:{type:"object",additionalProperties:false,required:["inputPath","outputPath"],properties:{inputPath:pathSchema,outputPath:pathSchema,duotone:{type:"array",minItems:2,maxItems:2,items:{type:"string",pattern:"^#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$"}},alphaCutoff:{type:"integer",minimum:1,maximum:64},timeoutMs:{type:"integer",minimum:1,maximum:300000}}},resources:[{resource:"cpu",amount:1},{resource:"local-io",amount:1}]},{code:"slopcamera.image.generate",title:"Generate image",description:"Generate one bounded image directly through Vercel AI Gateway with an environment credential and no client retry.",execution:"gateway",authentication:"environment",destructive:true,idempotent:false,inputSchema:{type:"object",additionalProperties:false,required:["model","prompt","outputPath"],properties:{model:modelSchema,prompt:{type:"string",minLength:1,maxLength:slopcameraMaximumPromptBytes},outputPath:pathSchema}},resources:[{resource:"local-io",amount:1},{resource:"network",amount:1},{resource:"paid-call",amount:1}],transport:{method:"POST",authority:"https://ai-gateway.vercel.sh/v4/ai",authorization:"bearer",retry:"never"}},{code:"slopcamera.image.icon",title:"Generate line-art icon",description:"Generate one isometric line-art SVG icon: a style-locked Vercel AI Gateway raster normalized to canonical ink-on-transparent pixels, traced locally, and optionally critiqued by a vision model across bounded rounds.",execution:"gateway",authentication:"environment",destructive:true,idempotent:false,inputSchema:{type:"object",additionalProperties:false,required:["subject","outputPath"],properties:{subject:{type:"string",minLength:1,maxLength:slopcameraIconSubjectMaximumBytes},outputPath:pathSchema,model:modelSchema,critiqueModel:modelSchema,ink:{type:"string",pattern:"^#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$"},purpose:{type:"string",enum:["mark","illustration"]},rounds:{type:"integer",minimum:1,maximum:slopcameraIconMaximumRounds},keepRaster:{type:"boolean"}}},resources:[{resource:"cpu",amount:1},{resource:"local-io",amount:1},{resource:"network",amount:1},{resource:"paid-call",amount:1}],transport:{method:"POST",authority:"https://ai-gateway.vercel.sh/v4/ai",authorization:"bearer",retry:"never"}},{code:"slopcamera.image.gallery",title:"Generate image gallery",description:"Generate several bounded Vercel AI Gateway image candidates in parallel and compose a labelled contact sheet with per-candidate provenance for explicit review and selection.",execution:"gateway",authentication:"environment",destructive:true,idempotent:false,inputSchema:{type:"object",additionalProperties:false,required:["subject","outputDir"],properties:{subject:{type:"string",minLength:1,maxLength:slopcameraGalleryLimits.subjectBytes},outputDir:pathSchema,kind:{type:"string",enum:[...slopcameraGalleryKinds]},model:modelSchema,count:{type:"integer",minimum:1,maximum:slopcameraGalleryLimits.candidates},vary:{type:"array",maxItems:slopcameraGalleryAxes.length,items:{anyOf:[{type:"string",enum:[...slopcameraGalleryAxes]},{type:"object",additionalProperties:false,required:["axis"],properties:{axis:{type:"string",enum:[...slopcameraGalleryAxes]},values:{type:"array",minItems:1,maxItems:slopcameraGalleryLimits.candidates,items:{type:"string",minLength:1,maxLength:slopcameraGalleryLimits.variantBytes}}}}]}},candidates:{type:"array",minItems:1,maxItems:slopcameraGalleryLimits.candidates,items:{type:"object",additionalProperties:false,required:["id"],properties:{id:{type:"string",minLength:1,maxLength:slopcameraGalleryLimits.idLength},prompt:{type:"string",minLength:1,maxLength:slopcameraGalleryLimits.candidatePromptBytes},variant:{type:"string",minLength:1,maxLength:slopcameraGalleryLimits.variantBytes}}}},cellEdge:{type:"integer",minimum:slopcameraGalleryLimits.cellEdgeMin,maximum:slopcameraGalleryLimits.cellEdgeMax},tiled:{type:"boolean"},timeoutMs:{type:"integer",minimum:1000,maximum:30*60000}}},resources:[{resource:"cpu",amount:1},{resource:"local-io",amount:1},{resource:"network",amount:1},{resource:"paid-call",amount:1}],transport:{method:"POST",authority:"https://ai-gateway.vercel.sh/v4/ai",authorization:"bearer",retry:"never"}}]);function operationFailure(message){throw new SlopcameraOperationError("INVALID_OPERATION_INPUT",message)}function isRecord4(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}function record(value,allowedKeys){if(!isRecord4(value))operationFailure("Operation input must be an object.");const unknown=Object.keys(value).filter((key)=>!allowedKeys.includes(key));if(unknown.length>0){operationFailure(`Unsupported operation input field: ${unknown[0]}.`)}return value}function pathValue(value,name){if(typeof value!=="string"||value.length<1||value.length>4096||value.includes("\x00")){operationFailure(`${name} must be a non-empty bounded local path.`)}return value}function parseCheck(value){const input=record(value,["path"]);return{path:pathValue(input.path,"path")}}function parseRender(value){const input=record(value,["path","outDirectory","scale"]);const scale=input.scale;if(scale!==undefined&&(typeof scale!=="number"||!Number.isFinite(scale)||scale<=0||scale>4)){operationFailure("scale must be greater than zero and no more than 4.")}return{path:pathValue(input.path,"path"),...input.outDirectory===undefined?{}:{outDirectory:pathValue(input.outDirectory,"outDirectory")},...scale===undefined?{}:{scale}}}function parseVectorize(value){const input=record(value,["inputPath","outputPath","duotone","alphaCutoff","timeoutMs"]);const inputPath=pathValue(input.inputPath,"inputPath");const outputPath=pathValue(input.outputPath,"outputPath");if(!outputPath.toLowerCase().endsWith(".svg")){operationFailure("outputPath must end in .svg.")}const duotone=input.duotone;if(duotone!==undefined&&(!Array.isArray(duotone)||duotone.length!==2||duotone.some((color)=>typeof color!=="string"||!/^#[a-f0-9]{3}(?:[a-f0-9]{3})?$/iu.test(color)))){operationFailure("duotone must contain exactly two #rgb or #rrggbb colors.")}const alphaCutoff=input.alphaCutoff;if(alphaCutoff!==undefined&&(!Number.isInteger(alphaCutoff)||alphaCutoff<1||alphaCutoff>64)){operationFailure("alphaCutoff must be an integer from 1 through 64.")}const timeoutMs=input.timeoutMs;if(timeoutMs!==undefined&&(!Number.isInteger(timeoutMs)||timeoutMs<1||timeoutMs>300000)){operationFailure("timeoutMs must be an integer from 1 through 300000.")}return{inputPath,outputPath,...duotone===undefined?{}:{duotone},...alphaCutoff===undefined?{}:{alphaCutoff},...timeoutMs===undefined?{}:{timeoutMs}}}function parseGenerate(value){const input=record(value,["model","prompt","outputPath"]);if(typeof input.model!=="string"||input.model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(input.model)){operationFailure("model must be a bounded Vercel AI Gateway provider/model id.")}if(typeof input.prompt!=="string"||input.prompt.trim().length<1||/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(input.prompt)||Buffer.byteLength(input.prompt,"utf8")>slopcameraMaximumPromptBytes){operationFailure(`prompt must be non-empty and no more than ${slopcameraMaximumPromptBytes} UTF-8 bytes.`)}const outputPath=pathValue(input.outputPath,"outputPath");if(!/\.(?:jpe?g|png|webp)$/iu.test(outputPath)){operationFailure("outputPath must end in .png, .jpg, .jpeg, or .webp.")}return{model:input.model,prompt:input.prompt,outputPath}}function parseIcon(value){const input=record(value,["subject","outputPath","model","critiqueModel","ink","rounds","keepRaster"]);if(typeof input.subject!=="string"||input.subject.trim().length<1||/[\u0000-\u001f\u007f]/u.test(input.subject)||Buffer.byteLength(input.subject,"utf8")>slopcameraIconSubjectMaximumBytes){operationFailure(`subject must be non-empty and no more than ${slopcameraIconSubjectMaximumBytes} UTF-8 bytes.`)}const outputPath=pathValue(input.outputPath,"outputPath");if(!outputPath.toLowerCase().endsWith(".svg")){operationFailure("outputPath must end in .svg.")}for(const name of["model","critiqueModel"]){const model=input[name];if(model!==undefined&&(typeof model!=="string"||model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model))){operationFailure(`${name} must be a bounded Vercel AI Gateway provider/model id.`)}}if(input.ink!==undefined&&(typeof input.ink!=="string"||!/^#[a-f0-9]{3}(?:[a-f0-9]{3})?$/iu.test(input.ink))){operationFailure("ink must be a #rgb or #rrggbb color.")}const rounds=input.rounds;if(rounds!==undefined&&(!Number.isInteger(rounds)||rounds<1||rounds>slopcameraIconMaximumRounds)){operationFailure(`rounds must be an integer from 1 through ${slopcameraIconMaximumRounds}.`)}if(input.keepRaster!==undefined&&typeof input.keepRaster!=="boolean"){operationFailure("keepRaster must be a boolean.")}return{subject:input.subject,outputPath,...input.model===undefined?{}:{model:input.model},...input.critiqueModel===undefined?{}:{critiqueModel:input.critiqueModel},...input.ink===undefined?{}:{ink:input.ink},...rounds===undefined?{}:{rounds},...input.keepRaster===undefined?{}:{keepRaster:input.keepRaster}}}function parseGallery(value){const input=record(value,["subject","outputDir","kind","model","count","vary","candidates","cellEdge","tiled","timeoutMs"]);if(typeof input.subject!=="string"||input.subject.trim().length<1||/[\u0000-\u001f\u007f]/u.test(input.subject)||Buffer.byteLength(input.subject,"utf8")>slopcameraGalleryLimits.subjectBytes){operationFailure(`subject must be non-empty and no more than ${slopcameraGalleryLimits.subjectBytes} UTF-8 bytes.`)}if(input.kind!==undefined&&(typeof input.kind!=="string"||!slopcameraGalleryKinds.includes(input.kind))){operationFailure(`kind must be one of: ${slopcameraGalleryKinds.join(", ")}.`)}if(input.model!==undefined&&(typeof input.model!=="string"||input.model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(input.model))){operationFailure("model must be a bounded Vercel AI Gateway provider/model id.")}if(input.count!==undefined&&(!Number.isInteger(input.count)||input.count<1||input.count>slopcameraGalleryLimits.candidates)){operationFailure(`count must be an integer from 1 through ${slopcameraGalleryLimits.candidates}.`)}if(input.vary!==undefined&&(!Array.isArray(input.vary)||input.vary.length<1||input.vary.length>slopcameraGalleryAxes.length)){operationFailure(`vary must be a list of at most ${slopcameraGalleryAxes.length} axes.`)}if(input.candidates!==undefined&&(!Array.isArray(input.candidates)||input.candidates.length<1||input.candidates.length>slopcameraGalleryLimits.candidates)){operationFailure(`candidates must contain 1 through ${slopcameraGalleryLimits.candidates} entries.`)}if(input.vary!==undefined&&input.candidates!==undefined){operationFailure("vary and candidates are mutually exclusive.")}if(input.count!==undefined&&input.candidates!==undefined){operationFailure("count and candidates are mutually exclusive.")}const cellEdge=input.cellEdge;if(cellEdge!==undefined&&(!Number.isInteger(cellEdge)||cellEdge<slopcameraGalleryLimits.cellEdgeMin||cellEdge>slopcameraGalleryLimits.cellEdgeMax)){operationFailure(`cellEdge must be an integer from ${slopcameraGalleryLimits.cellEdgeMin} through ${slopcameraGalleryLimits.cellEdgeMax}.`)}const timeoutMs=input.timeoutMs;if(timeoutMs!==undefined&&(!Number.isInteger(timeoutMs)||timeoutMs<1000||timeoutMs>30*60000)){operationFailure("timeoutMs must be an integer from 1000 through 1800000.")}if(input.tiled!==undefined&&typeof input.tiled!=="boolean"){operationFailure("tiled must be a boolean when set.")}return{subject:input.subject,outputDir:pathValue(input.outputDir,"outputDir"),...input.kind===undefined?{}:{kind:input.kind},...input.model===undefined?{}:{model:input.model},...input.count===undefined?{}:{count:input.count},...input.vary===undefined?{}:{vary:input.vary},...input.candidates===undefined?{}:{candidates:input.candidates},...cellEdge===undefined?{}:{cellEdge},...input.tiled===undefined?{}:{tiled:input.tiled},...timeoutMs===undefined?{}:{timeoutMs}}}function parseSlopcameraOperationInput(code,input){switch(code){case"slopcamera.diagram.check":return parseCheck(input);case"slopcamera.diagram.render":return parseRender(input);case"slopcamera.image.vectorize":return parseVectorize(input);case"slopcamera.image.generate":return parseGenerate(input);case"slopcamera.image.icon":return parseIcon(input);case"slopcamera.image.gallery":return parseGallery(input);default:throw new SlopcameraOperationError("INVALID_OPERATION","Unknown Slopcamera operation code.")}}function isSlopcameraOperationCode(value){return slopcameraOperationCodes.includes(value)}function slopcameraOperationHostResourceClaims(code){const descriptor=slopcameraOperationRegistry.find((candidate)=>candidate.code===code);if(descriptor===undefined){throw new SlopcameraOperationError("INVALID_OPERATION","Unknown Slopcamera operation code.")}return descriptor.resources}function searchSlopcameraOperations(query="",limit=slopcameraOperationRegistry.length){if(typeof query!=="string"||query.length>200||/[\u0000-\u001f\u007f]/u.test(query)||!Number.isInteger(limit)||limit<1||limit>20){throw new SlopcameraOperationError("INVALID_SEARCH","Search requires a bounded query and a limit from 1 through 20.")}const terms=query.toLowerCase().split(/\s+/u).filter((term)=>term.length>0);return slopcameraOperationRegistry.filter((operation)=>{const haystack=`${operation.code} ${operation.title} ${operation.description}`.toLowerCase();return terms.every((term)=>haystack.includes(term))}).slice(0,limit)}function operationDependenciesWithLease(dependencies,lease){const inheritedFileDescriptors=[...dependencies.inheritedFileDescriptors??[],lease.inheritedFileDescriptor].filter((descriptor,index,descriptors)=>descriptors.indexOf(descriptor)===index);if(inheritedFileDescriptors.length>16||inheritedFileDescriptors.some((descriptor)=>!Number.isSafeInteger(descriptor)||descriptor<0||descriptor>2147483647)){throw new SlopcameraOperationError("INVALID_OPERATION_INPUT","Operation host-resource inheritance exceeds its descriptor bound.")}const{hostResourceCoordinator:_hostResourceCoordinator,signal:_signal,waitTimeoutMilliseconds:_waitTimeoutMilliseconds,...operationDependencies}=dependencies;return{...operationDependencies,inheritedFileDescriptors}}async function withSlopcameraOperationHostAdmission(code,callback,options={}){const coordinator=options.hostResourceCoordinator??createDefaultHostResourceCoordinator();return await coordinator.withLease(slopcameraOperationHostResourceClaims(code),async(lease)=>{await lease.assertOwned();return await callback(lease)},{...options.signal===undefined?{}:{signal:options.signal},...options.waitTimeoutMilliseconds===undefined?{}:{waitTimeoutMilliseconds:options.waitTimeoutMilliseconds}})}var operationBuiltInConfig=Object.freeze({icons:builtInIcons});async function readOperationDiagram(path){const absolutePath=resolve3(path);let value;try{value=JSON.parse(await readFile2(absolutePath,"utf8"))}catch(cause){throw new SlopcameraOperationError("INVALID_OPERATION_INPUT","Diagram source could not be read as JSON.")}const spec=parseDiagramSpec(value);for(const shape of spec.shapes){if((shape.type==="rect"||shape.type==="ellipse")&&shape.icon!==undefined&&!Object.hasOwn(builtInIcons,shape.icon)){throw new SlopcameraOperationError("INVALID_OPERATION_INPUT","Diagram requests an unavailable built-in icon.")}}return{absolutePath,spec}}async function atomicOperationWrite(path,value){const temporaryPath=join2(dirname3(path),`.${randomUUID3()}.slopcamera-operation.tmp`);try{await writeFile3(temporaryPath,value,{flag:"wx"});await rename2(temporaryPath,path)}finally{await rm3(temporaryPath,{force:true}).catch(()=>{return})}}async function checkOperationDiagram(path){const{spec}=await readOperationDiagram(path);return{findings:lintDiagram(spec),configPath:null}}async function renderOperationDiagram(input){const{absolutePath,spec}=await readOperationDiagram(input.path);const outputDirectory=resolve3(input.outDirectory??dirname3(absolutePath));const scale=input.scale??2;const[light,dark]=await Promise.all([renderSvg(spec,"light",operationBuiltInConfig),renderSvg(spec,"dark",operationBuiltInConfig)]);const[lightPng,darkPng]=[renderPng(light,operationBuiltInConfig,scale),renderPng(dark,operationBuiltInConfig,scale)];const artifacts={spec:absolutePath,tldr:join2(outputDirectory,`${spec.name}.tldr`),lightSvg:join2(outputDirectory,`${spec.name}.light.svg`),darkSvg:join2(outputDirectory,`${spec.name}.dark.svg`),lightPng:join2(outputDirectory,`${spec.name}.light.png`),darkPng:join2(outputDirectory,`${spec.name}.dark.png`)};await mkdir3(outputDirectory,{recursive:true});await Promise.all([atomicOperationWrite(artifacts.tldr,serializeTldr(spec,operationBuiltInConfig)),atomicOperationWrite(artifacts.lightSvg,light.svg),atomicOperationWrite(artifacts.darkSvg,dark.svg),atomicOperationWrite(artifacts.lightPng,lightPng),atomicOperationWrite(artifacts.darkPng,darkPng)]);return{artifacts,findings:lintDiagram(spec),configPath:null}}async function executeSlopcameraOperationUncoordinated(code,value,dependencies={}){const input=parseSlopcameraOperationInput(code,value);switch(code){case"slopcamera.diagram.check":{const options=input;return await checkOperationDiagram(options.path)}case"slopcamera.diagram.render":{const options=input;return await renderOperationDiagram(options)}case"slopcamera.image.vectorize":{const options=input;const result=await vectorizeImage(options.inputPath,{outputPath:options.outputPath,...options.duotone===undefined?{}:{duotone:options.duotone},...options.alphaCutoff===undefined?{}:{alphaCutoff:options.alphaCutoff},...options.timeoutMs===undefined?{}:{limits:{maxDurationMs:options.timeoutMs}},...dependencies.inheritedFileDescriptors===undefined?{}:{inheritedFileDescriptors:dependencies.inheritedFileDescriptors}});if(result.outputPath===null){throw new SlopcameraOperationError("INVALID_OPERATION_INPUT","Vectorization did not publish its required output.")}return{outputPath:result.outputPath,receipt:result.receipt}}case"slopcamera.image.generate":{const options=input;return await generateSlopcameraImageFile({...options,...dependencies.signal===undefined?{}:{signal:dependencies.signal}},dependencies)}case"slopcamera.image.icon":{const options=input;return await generateSlopcameraIcon({...options,...dependencies.signal===undefined?{}:{signal:dependencies.signal},...dependencies.inheritedFileDescriptors===undefined?{}:{inheritedFileDescriptors:dependencies.inheritedFileDescriptors}},dependencies)}case"slopcamera.image.gallery":{const options=input;return await generateSlopcameraImageGallery({...options,...dependencies.signal===undefined?{}:{signal:dependencies.signal}},dependencies)}default:throw new SlopcameraOperationError("INVALID_OPERATION","Unknown Slopcamera operation code.")}}async function executeSlopcameraOperationWithLease(code,value,lease,dependencies={}){await lease.assertOwned();const available=new Map;for(const claim of lease.claims){if(typeof claim.resource!=="string"||!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(claim.resource)||!Number.isSafeInteger(claim.amount)||claim.amount<1){throw new SlopcameraOperationError("INVALID_OPERATION","The active host-resource lease contains invalid claims.")}const total=(available.get(claim.resource)??0)+claim.amount;if(!Number.isSafeInteger(total)){throw new SlopcameraOperationError("INVALID_OPERATION","The active host-resource lease contains invalid claims.")}available.set(claim.resource,total)}const missing=slopcameraOperationHostResourceClaims(code).filter((claim)=>(available.get(claim.resource)??0)<claim.amount);if(missing.length>0){throw new SlopcameraOperationError("INVALID_OPERATION",`The active host-resource lease does not cover ${missing.map((claim)=>`${claim.resource}:${String(claim.amount)}`).join(", ")}.`)}return await executeSlopcameraOperationUncoordinated(code,value,operationDependenciesWithLease(dependencies,lease))}async function executeSlopcameraOperation(code,value,dependencies={}){const input=parseSlopcameraOperationInput(code,value);return await withSlopcameraOperationHostAdmission(code,async(lease)=>await executeSlopcameraOperationUncoordinated(code,input,operationDependenciesWithLease(dependencies,lease)),dependencies)}
|
|
35
|
-
export{builtInIcons,sanitizeIcon,resolveEdge,renderSvg,renderPng,lintDiagram,stackLayoutDefaults,StackLayoutError,resolveStackLayout,resolveDiagramSource,DiagramValidationError,parseDiagramSource,parseDiagramSpec,serializeTldr,slopcameraIconDefaultInk,slopcameraIconPanel,slopcameraIconDefaultRounds,slopcameraIconMaximumRounds,slopcameraIconSubjectMaximumBytes,slopcameraIconCritiqueDefaultModel,slopcameraIconCritiqueTimeoutMs,iconPromptFor,iconVisualGateProblems,extractIconLineArt,critiqueIconRaster,generateSlopcameraIcon,slopcameraGalleryKinds,slopcameraGalleryAxes,slopcameraGalleryLimits,galleryCandidateId,galleryPromptFor,planSlopcameraGallery,parseSlopcameraGalleryVary,generateSlopcameraImageGallery,composeSlopcameraImageGallery,slopcameraOperationCodes,SlopcameraOperationError,slopcameraOperationRegistry,parseSlopcameraOperationInput,isSlopcameraOperationCode,slopcameraOperationHostResourceClaims,searchSlopcameraOperations,withSlopcameraOperationHostAdmission,executeSlopcameraOperationWithLease,executeSlopcameraOperation};
|
|
35
|
+
export{builtInIcons,sanitizeIcon,layoutBoxContent,NebulaSans_Bold_default,NebulaSans_Book_default,resolveEdge,renderSvg,renderPng,lintDiagram,stackLayoutDefaults,StackLayoutError,resolveStackLayout,resolveDiagramSource,DiagramValidationError,parseDiagramSource,parseDiagramSpec,serializeTldr,slopcameraIconDefaultInk,slopcameraIconPanel,slopcameraIconDefaultRounds,slopcameraIconMaximumRounds,slopcameraIconSubjectMaximumBytes,slopcameraIconCritiqueDefaultModel,slopcameraIconCritiqueTimeoutMs,iconPromptFor,iconVisualGateProblems,extractIconLineArt,critiqueIconRaster,generateSlopcameraIcon,slopcameraGalleryKinds,slopcameraGalleryAxes,slopcameraGalleryLimits,galleryCandidateId,galleryPromptFor,planSlopcameraGallery,parseSlopcameraGalleryVary,generateSlopcameraImageGallery,composeSlopcameraImageGallery,slopcameraOperationCodes,SlopcameraOperationError,slopcameraOperationRegistry,parseSlopcameraOperationInput,isSlopcameraOperationCode,slopcameraOperationHostResourceClaims,searchSlopcameraOperations,withSlopcameraOperationHostAdmission,executeSlopcameraOperationWithLease,executeSlopcameraOperation};
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS,SLOPCAMERA_PORTABLE_CAPABILITY_VERSION,SLOPCAMERA_VERSION,STUDIO_LIMITS,SlopcameraCapabilityCommandSchema,SlopcameraCapabilityEffectSchema,SlopcameraCapabilityManifestSchema,SlopcameraCapabilityModuleDeclarationSchema,SlopcameraCapabilityModuleSchema,SlopcameraCapabilityOperationSchema,SlopcameraCapabilityPreparationSchema,SlopcameraCapabilityProfileSchema,SlopcameraCapabilityQualificationSchema,SlopcameraCapabilityResourceSchema,SlopcameraCapabilityResumeSchema,SlopcameraCapabilityToolSchema,SlopcameraCapabilityWorkflowSchema,SlopcameraMcpToolRuntime,SlopcameraSceneVariantsSchema,StudioCapabilityNameSchema,StudioDigestSchema,StudioEngineOptionsSchema,StudioEngineSchema,StudioExecutionProfileSchema,StudioJobSchema,StudioOutputArtifactSchema,StudioOutputFormatSchema,StudioOutputInterpretationSchema,StudioOutputRoleSchema,StudioOutputSpecSchema,StudioPathSchema,StudioPlanSchema,StudioReceiptSchema,StudioRenderSchema,StudioRuntimeIdentitySchema,StudioSourceBundleSchema,StudioSourceSpaceSchema,WorkspaceBoundary,WorkspaceBoundaryError,artifactSummary,checkDiagramFile,compileSlopcameraCapabilityManifest,createPortableSlopcameraCapabilityManifest,defineSlopcameraCapabilityModule,diagramApi,inspectStudioBundle,inspectStudioPlan,mcpMaximumRenderedPixels,mcpMaximumScale,mcpSourceByteLimit,parseSlopcameraCapabilityManifest,parseSlopcameraSceneVariants,parseStudioJob,parseStudioPlan,parseStudioReceipt,parseStudioRuntimeIdentity,parseStudioSourceBundle,planSlopcameraSceneGallery,planStudioJob,readDiagramFile,renderDiagramFile,runMcpServer,slopcameraApi,slopcameraMcpProtocolVersion,slopcameraMcpServerName,slopcameraMcpTools,slopcameraPortableCapabilityManifest,slopcameraSceneGalleryLimits,studioJobSha256,studioOutputPath,studioRuntimeSha256,studioSourceBundleSha256,summarizeSceneVariantPatch,validateStudioReceipt}from"./index-
|
|
2
|
+
import{DrawingValidationError,SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS,SLOPCAMERA_PORTABLE_CAPABILITY_VERSION,SLOPCAMERA_VERSION,STUDIO_LIMITS,SlopcameraCapabilityCommandSchema,SlopcameraCapabilityEffectSchema,SlopcameraCapabilityManifestSchema,SlopcameraCapabilityModuleDeclarationSchema,SlopcameraCapabilityModuleSchema,SlopcameraCapabilityOperationSchema,SlopcameraCapabilityPreparationSchema,SlopcameraCapabilityProfileSchema,SlopcameraCapabilityQualificationSchema,SlopcameraCapabilityResourceSchema,SlopcameraCapabilityResumeSchema,SlopcameraCapabilityToolSchema,SlopcameraCapabilityWorkflowSchema,SlopcameraMcpToolRuntime,SlopcameraSceneVariantsSchema,StudioCapabilityNameSchema,StudioDigestSchema,StudioEngineOptionsSchema,StudioEngineSchema,StudioExecutionProfileSchema,StudioJobSchema,StudioOutputArtifactSchema,StudioOutputFormatSchema,StudioOutputInterpretationSchema,StudioOutputRoleSchema,StudioOutputSpecSchema,StudioPathSchema,StudioPlanSchema,StudioReceiptSchema,StudioRenderSchema,StudioRuntimeIdentitySchema,StudioSourceBundleSchema,StudioSourceSpaceSchema,WorkspaceBoundary,WorkspaceBoundaryError,artifactSummary,checkDiagramFile,checkDrawingFile,compileSlopcameraCapabilityManifest,createPortableSlopcameraCapabilityManifest,defineSlopcameraCapabilityModule,diagramApi,drawingMinimumGlyphHeightPt,drawingPageGeometry,drawingProfile,drawingSourceByteLimit,inspectStudioBundle,inspectStudioPlan,mcpMaximumRenderedPixels,mcpMaximumScale,mcpSourceByteLimit,parseDrawingSource,parseSlopcameraCapabilityManifest,parseSlopcameraSceneVariants,parseStudioJob,parseStudioPlan,parseStudioReceipt,parseStudioRuntimeIdentity,parseStudioSourceBundle,planSlopcameraSceneGallery,planStudioJob,readDiagramFile,readDrawingFile,renderDiagramFile,renderDrawing,renderDrawingFile,runMcpServer,slopcameraApi,slopcameraMcpProtocolVersion,slopcameraMcpServerName,slopcameraMcpTools,slopcameraPortableCapabilityManifest,slopcameraSceneGalleryLimits,starterDrawingSource,studioJobSha256,studioOutputPath,studioRuntimeSha256,studioSourceBundleSha256,summarizeSceneVariantPatch,validateStudioReceipt}from"./index-10bz03js.js";import{bundledSkillPath,installSkill}from"./index-grk47tgs.js";import"./index-z1kd1x8j.js";import"./index-3cxf2c58.js";import{SlopcameraWorkflowError,defineSlopcameraWorkflow,runSlopcameraWorkflow}from"./index-qtn3s421.js";import{DiagramValidationError,SlopcameraOperationError,StackLayoutError,builtInIcons,composeSlopcameraImageGallery,critiqueIconRaster,executeSlopcameraOperation,executeSlopcameraOperationWithLease,extractIconLineArt,galleryCandidateId,galleryPromptFor,generateSlopcameraIcon,generateSlopcameraImageGallery,iconPromptFor,iconVisualGateProblems,isSlopcameraOperationCode,lintDiagram,parseDiagramSource,parseDiagramSpec,parseSlopcameraGalleryVary,parseSlopcameraOperationInput,planSlopcameraGallery,renderPng,renderSvg,resolveDiagramSource,resolveEdge,resolveStackLayout,searchSlopcameraOperations,serializeTldr,slopcameraGalleryAxes,slopcameraGalleryKinds,slopcameraGalleryLimits,slopcameraIconCritiqueDefaultModel,slopcameraIconCritiqueTimeoutMs,slopcameraIconDefaultInk,slopcameraIconDefaultRounds,slopcameraIconMaximumRounds,slopcameraIconPanel,slopcameraIconSubjectMaximumBytes,slopcameraOperationCodes,slopcameraOperationHostResourceClaims,slopcameraOperationRegistry,stackLayoutDefaults,withSlopcameraOperationHostAdmission}from"./index-sy1n4zfy.js";import{VTRACER_VERSION,VectorizeError,vectorizeDefaultLimits,vectorizeHardLimits,vectorizeImage,vectorizeProfileNames,vtracerReleases}from"./index-qry58nj2.js";import{SlopcameraCloudError,createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraGatewayCredentialStatus,slopcameraImageModels,slopcameraMaximumPromptBytes,slopcameraMaximumRawImageBytes,slopcameraResponseMediaTypes}from"./index-77fjfg9f.js";import{HOST_RESOURCE_MAX_WAIT_MILLISECONDS,HostResourceError,createDefaultHostResourceCoordinator,createHostResourceCoordinator,createProcessLocalHostResourceCoordinator,defaultSlopcameraHostResourceProfile,defaultSlopcameraHostResourceStateRoot,normalizeHostResourceClaims,normalizeHostResourceProfile,slopcameraHostResourceNames}from"./index-b3geqczn.js";import"./index-80a36bc0.js";export{withSlopcameraOperationHostAdmission,vtracerReleases,vectorizeProfileNames,vectorizeImage,vectorizeHardLimits,vectorizeDefaultLimits,validateStudioReceipt,summarizeSceneVariantPatch,studioSourceBundleSha256,studioRuntimeSha256,studioOutputPath,studioJobSha256,starterDrawingSource,stackLayoutDefaults,slopcameraSceneGalleryLimits,slopcameraResponseMediaTypes,slopcameraPortableCapabilityManifest,slopcameraOperationRegistry,slopcameraOperationHostResourceClaims,slopcameraOperationCodes,slopcameraMcpTools,slopcameraMcpServerName,slopcameraMcpProtocolVersion,slopcameraMaximumRawImageBytes,slopcameraMaximumPromptBytes,slopcameraImageModels,slopcameraIconSubjectMaximumBytes,slopcameraIconPanel,slopcameraIconMaximumRounds,slopcameraIconDefaultRounds,slopcameraIconDefaultInk,slopcameraIconCritiqueTimeoutMs,slopcameraIconCritiqueDefaultModel,slopcameraHostResourceNames,slopcameraGatewayCredentialStatus,slopcameraGatewayApiBaseUrl,slopcameraGalleryLimits,slopcameraGalleryKinds,slopcameraGalleryAxes,slopcameraApi,serializeTldr,searchSlopcameraOperations,runSlopcameraWorkflow,runMcpServer,resolveStackLayout,resolveSlopcameraGatewayCredential,resolveEdge,resolveDiagramSource,renderSvg,renderPng,renderDrawingFile,renderDrawing,renderDiagramFile,readDrawingFile,readDiagramFile,planStudioJob,planSlopcameraSceneGallery,planSlopcameraGallery,parseStudioSourceBundle,parseStudioRuntimeIdentity,parseStudioReceipt,parseStudioPlan,parseStudioJob,parseSlopcameraSceneVariants,parseSlopcameraOperationInput,parseSlopcameraGalleryVary,parseSlopcameraCapabilityManifest,parseDrawingSource,parseDiagramSpec,parseDiagramSource,normalizeHostResourceProfile,normalizeHostResourceClaims,mcpSourceByteLimit,mcpMaximumScale,mcpMaximumRenderedPixels,lintDiagram,isSlopcameraOperationCode,installSkill,inspectStudioPlan,inspectStudioBundle,iconVisualGateProblems,iconPromptFor,generateSlopcameraImageGallery,generateSlopcameraImageFile,generateSlopcameraImage,generateSlopcameraIcon,galleryPromptFor,galleryCandidateId,extractIconLineArt,executeSlopcameraOperationWithLease,executeSlopcameraOperation,drawingSourceByteLimit,drawingProfile,drawingPageGeometry,drawingMinimumGlyphHeightPt,diagramApi,defineSlopcameraWorkflow,defineSlopcameraCapabilityModule,defaultSlopcameraHostResourceStateRoot,defaultSlopcameraHostResourceProfile,critiqueIconRaster,createProcessLocalHostResourceCoordinator,createPortableSlopcameraCapabilityManifest,createHostResourceCoordinator,createFixedGatewayFetch,createDefaultHostResourceCoordinator,composeSlopcameraImageGallery,compileSlopcameraCapabilityManifest,checkDrawingFile,checkDiagramFile,bundledSkillPath,builtInIcons,artifactSummary,WorkspaceBoundaryError,WorkspaceBoundary,VectorizeError,VTRACER_VERSION,StudioSourceSpaceSchema,StudioSourceBundleSchema,StudioRuntimeIdentitySchema,StudioRenderSchema,StudioReceiptSchema,StudioPlanSchema,StudioPathSchema,StudioOutputSpecSchema,StudioOutputRoleSchema,StudioOutputInterpretationSchema,StudioOutputFormatSchema,StudioOutputArtifactSchema,StudioJobSchema,StudioExecutionProfileSchema,StudioEngineSchema,StudioEngineOptionsSchema,StudioDigestSchema,StudioCapabilityNameSchema,StackLayoutError,SlopcameraWorkflowError,SlopcameraSceneVariantsSchema,SlopcameraOperationError,SlopcameraMcpToolRuntime,SlopcameraCloudError,SlopcameraCapabilityWorkflowSchema,SlopcameraCapabilityToolSchema,SlopcameraCapabilityResumeSchema,SlopcameraCapabilityResourceSchema,SlopcameraCapabilityQualificationSchema,SlopcameraCapabilityProfileSchema,SlopcameraCapabilityPreparationSchema,SlopcameraCapabilityOperationSchema,SlopcameraCapabilityModuleSchema,SlopcameraCapabilityModuleDeclarationSchema,SlopcameraCapabilityManifestSchema,SlopcameraCapabilityEffectSchema,SlopcameraCapabilityCommandSchema,STUDIO_LIMITS,SLOPCAMERA_VERSION,SLOPCAMERA_PORTABLE_CAPABILITY_VERSION,SLOPCAMERA_CAPABILITY_MANIFEST_LIMITS,HostResourceError,HOST_RESOURCE_MAX_WAIT_MILLISECONDS,DrawingValidationError,DiagramValidationError};
|
package/dist/operations.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{SlopcameraOperationError,executeSlopcameraOperation,executeSlopcameraOperationWithLease,isSlopcameraOperationCode,parseSlopcameraOperationInput,searchSlopcameraOperations,slopcameraOperationCodes,slopcameraOperationHostResourceClaims,slopcameraOperationRegistry,withSlopcameraOperationHostAdmission}from"./index-
|
|
2
|
+
import{SlopcameraOperationError,executeSlopcameraOperation,executeSlopcameraOperationWithLease,isSlopcameraOperationCode,parseSlopcameraOperationInput,searchSlopcameraOperations,slopcameraOperationCodes,slopcameraOperationHostResourceClaims,slopcameraOperationRegistry,withSlopcameraOperationHostAdmission}from"./index-sy1n4zfy.js";import"./index-qry58nj2.js";import"./index-77fjfg9f.js";import"./index-b3geqczn.js";import"./index-80a36bc0.js";export{withSlopcameraOperationHostAdmission,slopcameraOperationRegistry,slopcameraOperationHostResourceClaims,slopcameraOperationCodes,searchSlopcameraOperations,parseSlopcameraOperationInput,isSlopcameraOperationCode,executeSlopcameraOperationWithLease,executeSlopcameraOperation,SlopcameraOperationError};
|
package/dist/workflow.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{SlopcameraWorkflowError,defineSlopcameraWorkflow,runSlopcameraWorkflow}from"./index-
|
|
2
|
+
import{SlopcameraWorkflowError,defineSlopcameraWorkflow,runSlopcameraWorkflow}from"./index-qtn3s421.js";import"./index-sy1n4zfy.js";import"./index-qry58nj2.js";import"./index-77fjfg9f.js";import"./index-b3geqczn.js";import"./index-80a36bc0.js";export{runSlopcameraWorkflow,defineSlopcameraWorkflow,SlopcameraWorkflowError};
|
package/docs/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Install the [verified Slopcamera v3.3.2 release](../README.md#install-slopcamera
|
|
|
13
13
|
## Complete a task
|
|
14
14
|
|
|
15
15
|
- [Run current-source commands](how-to/use-current-source.md): build an exact checkout of Slopcamera.
|
|
16
|
+
- [Prepare patent-style drawing sheets](how-to/patent-drawings.md): retain diagram source, check physical bounds, and render monochrome SVG sheets and a PDF; requires current source.
|
|
16
17
|
|
|
17
18
|
- [Render motion graphics from HTML](how-to/render-motion-graphics.md): choose among seven authoring profiles, render a graphic, and retain its source.
|
|
18
19
|
- [Edit and deliver video](how-to/edit-video.md): import footage, align related tracks, place overlays, and check a delivery.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/slopcamera",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.6",
|
|
4
4
|
"description": "A local visual studio for coding agents: author scenes, combine generated and recorded media, and export images, diagrams, animation, and video from retained sources.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/hraness/slopcamera/main/schema/drawing.schema.json",
|
|
4
|
+
"title": "Slopcamera drawing sheets, version 1",
|
|
5
|
+
"description": "Editable physical drawing sheets containing existing version-one diagrams. Runtime profile checks additionally require monochrome uppercase labels, measured glyph heights, physical ink bounds, and consecutive figure numbers.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "name", "paper", "sheets"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": { "type": "string" },
|
|
11
|
+
"version": { "const": 1 },
|
|
12
|
+
"name": { "type": "string", "maxLength": 100, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
13
|
+
"title": { "type": "string", "minLength": 1, "maxLength": 1024 },
|
|
14
|
+
"paper": { "enum": ["us-letter", "a4"] },
|
|
15
|
+
"sheets": {
|
|
16
|
+
"type": "array", "minItems": 1, "maxItems": 20,
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object", "additionalProperties": false,
|
|
19
|
+
"required": ["figure", "diagram"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"figure": { "type": "integer", "minimum": 1, "maximum": 20 },
|
|
22
|
+
"diagram": { "$ref": "https://raw.githubusercontent.com/hraness/slopcamera/main/schema/diagram.schema.json" }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -18,6 +18,7 @@ Slopcamera installs from its verified release archive or from source. Historical
|
|
|
18
18
|
| Requested result | Read |
|
|
19
19
|
| --- | --- |
|
|
20
20
|
| Editable diagram, flowchart or visual explanation | [Diagrams](references/diagrams.md) and [visual communication](references/visual-communication.md) |
|
|
21
|
+
| Patent-style drawing sheets, physical margins, monochrome SVGs or a drawing PDF | [Drawing sheets](references/patent-drawings.md); requires current source |
|
|
21
22
|
| Custom diagram themes, fonts or icon packages | [Customization](references/customization.md) |
|
|
22
23
|
| Raster artwork converted to SVG | [Vectorization](references/vectorization.md) |
|
|
23
24
|
| Record, clean up, caption, frame or deliver video | [Video projects](references/video-projects.md) |
|
|
@@ -14,7 +14,7 @@ command -v slopcamera
|
|
|
14
14
|
|
|
15
15
|
A restricted shell can omit package-manager paths. Check known host installation paths before declaring a tool unavailable. If Bun is genuinely absent, follow its official [installation guide](https://bun.sh/docs/installation) within the user’s authorized setup scope. Do not switch package managers or pipe an unreviewed installer into a shell.
|
|
16
16
|
|
|
17
|
-
Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.
|
|
17
|
+
Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.5/hraness-slopcamera-3.3.5.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
|
|
18
18
|
|
|
19
19
|
Use an existing compatible source build when available. Otherwise follow the [complete source-install guide](https://github.com/hraness/slopcamera/blob/main/docs/how-to/use-current-source.md): clone into a new directory, record its exact commit, install locked dependencies without lifecycle scripts, build the SDK and source CLI, then define the shell command against that checkout. A clone or skill installation alone does not install the executable. Do not alter an existing active checkout to satisfy this path.
|
|
20
20
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Prepare patent-style drawing sheets
|
|
2
|
+
|
|
3
|
+
Use `diagram sheets` for local monochrome vector drawing sheets with physical
|
|
4
|
+
page dimensions. These commands require current source; do not assume a
|
|
5
|
+
previously published archive includes them. Confirm `slopcamera help diagram`
|
|
6
|
+
lists the commands. If absent, follow [installation](install.md) for an explicit
|
|
7
|
+
source build rather than inventing a flag on ordinary diagram rendering.
|
|
8
|
+
|
|
9
|
+
## Preserve the authored content
|
|
10
|
+
|
|
11
|
+
Find the existing `.drawing.json` first. It is the authority for labels,
|
|
12
|
+
reference numerals, geometry, relationships, and sheet order. Never infer an
|
|
13
|
+
invention, assign a component's reference numeral, or expand a claim while
|
|
14
|
+
changing its presentation. Keep private drawings and terminology out of public
|
|
15
|
+
examples and remote services.
|
|
16
|
+
|
|
17
|
+
The version-one drawing document contains `name`, `paper: "a4" | "us-letter"`,
|
|
18
|
+
an optional metadata-only `title`, and `sheets`. Each sheet has a consecutive
|
|
19
|
+
`figure` number beginning at one and an ordinary version-one `diagram` object.
|
|
20
|
+
There is one figure per sheet.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
slopcamera diagram sheets init sensor-control.drawing.json --json
|
|
24
|
+
slopcamera diagram sheets check sensor-control.drawing.json --json
|
|
25
|
+
slopcamera diagram sheets render sensor-control.drawing.json --out-dir drawings --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run `init` only for new source; it refuses an existing destination. Rendering
|
|
29
|
+
returns artifact paths for the document-name PDF, per-sheet SVGs, and drawing
|
|
30
|
+
receipt. Retain the JSON and receipt. Make revisions in the source and rerender.
|
|
31
|
+
|
|
32
|
+
## Fit and review the sheets
|
|
33
|
+
|
|
34
|
+
Coordinates and font sizes use points. The profile does not scale artwork to
|
|
35
|
+
fit. It uses one bundled outlined font, black strokes, a white page, and explicit
|
|
36
|
+
physical bounds. Uppercase ASCII letters and digits must have at least 3.2 mm of
|
|
37
|
+
visible height. Custom fonts, monospace roles, icons, color, and transparency are
|
|
38
|
+
outside this profile. Authored text is limited to 18 pt; sheet and figure numbers
|
|
39
|
+
use 20 pt. Preserve authored meaning when shortening labels or repositioning
|
|
40
|
+
elements.
|
|
41
|
+
|
|
42
|
+
Inspect every SVG and PDF sheet at full size and two-thirds size. Check labels,
|
|
43
|
+
connectors, reference-numeral consistency with the specification, and reading
|
|
44
|
+
order. Outlined text will not be selectable; inspect pixels and preserve the
|
|
45
|
+
source instead of treating text extraction as a visual check.
|
|
46
|
+
|
|
47
|
+
The profile follows physical constraints drawn from
|
|
48
|
+
[37 CFR 1.84](https://www.uspto.gov/web/offices/pac/mpep/consolidated_rules.pdf).
|
|
49
|
+
It uses US-style figure numbering, including omission of `FIG.` for a single
|
|
50
|
+
view. [PCT Rule 11](https://www.wipo.int/en/web/pct-system/texts/rules/r11) has its
|
|
51
|
+
own formatting requirements; A4 paper alone does not establish PCT compliance.
|
|
52
|
+
Report checked geometry and provenance separately from technical disclosure or
|
|
53
|
+
filing approval. A receipt does not establish patentability or legal compliance.
|
package/src/cli.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import type { HostResourceCoordinator } from "./host-resources.js"
|
|
37
37
|
import { installSkill, type SkillScope, type SkillTarget } from "./skill-install.js"
|
|
38
38
|
import { pathExists } from "./fs.js"
|
|
39
|
+
import { checkDrawingFile, renderDrawingFile, starterDrawingSource } from "./drawing.js"
|
|
39
40
|
import { SLOPCAMERA_VERSION } from "./version.js"
|
|
40
41
|
import { reportUsefulResult, type UsefulResultObserver } from "./support-completion.js"
|
|
41
42
|
import { runProductSupportCommand, showProductSupportInvitation, standaloneSupportEnvironment } from "./support.js"
|
|
@@ -50,6 +51,9 @@ Usage:
|
|
|
50
51
|
slopcamera diagram init [file]
|
|
51
52
|
slopcamera diagram check <file> [--config <file>] [--strict]
|
|
52
53
|
slopcamera diagram render <file> [--out-dir <directory>] [--config <file>] [--scale <number>]
|
|
54
|
+
slopcamera diagram sheets init <file.drawing.json> [--json]
|
|
55
|
+
slopcamera diagram sheets check <file.drawing.json> [--json]
|
|
56
|
+
slopcamera diagram sheets render <file.drawing.json> [--out-dir <directory>] [--json]
|
|
53
57
|
slopcamera image vectorize <image> --output <file.svg> [--json] [--duotone <#rgb,#rgb>]
|
|
54
58
|
slopcamera image generate <prompt> --output <file.png|jpg|webp> [--model <provider/model>] [--json]
|
|
55
59
|
slopcamera image icon <subject> --output <file.svg> [--purpose <mark|illustration>]
|
|
@@ -66,7 +70,7 @@ Usage:
|
|
|
66
70
|
slopcamera skill path
|
|
67
71
|
slopcamera skill install [--target codex|claude|agents] [--scope user|project] [--force]
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
Diagram render writes the same five replaceable artifacts on every run:
|
|
70
74
|
<name>.tldr
|
|
71
75
|
<name>.light.svg
|
|
72
76
|
<name>.dark.svg
|
|
@@ -76,6 +80,11 @@ Render writes the same five replaceable artifacts on every run:
|
|
|
76
80
|
The .tldr file is editable interchange for browser-based canvas tooling.
|
|
77
81
|
Rendering does not require a desktop application or a bundled UI runtime.
|
|
78
82
|
|
|
83
|
+
Diagram sheets creates bounded black-and-white drawing sheets from a separate
|
|
84
|
+
.drawing.json source: one vector PDF, one outlined SVG per sheet, and a receipt.
|
|
85
|
+
Check is read-only; init never overwrites. Geometry checks do not certify legal
|
|
86
|
+
compliance. This surface is available through the CLI and root SDK.
|
|
87
|
+
|
|
79
88
|
Vectorize adaptively traces a raster with a checksum-pinned VTracer binary.
|
|
80
89
|
It enforces bounded input, decode, time, path, and output budgets and emits a
|
|
81
90
|
safe path-only SVG (plus an internal vector alpha mask when fidelity requires).
|
|
@@ -251,13 +260,88 @@ function hostAdmissionOptions(
|
|
|
251
260
|
: { hostResourceCoordinator: dependencies.hostResourceCoordinator }
|
|
252
261
|
}
|
|
253
262
|
|
|
263
|
+
async function runDrawingSheets(
|
|
264
|
+
args: readonly string[],
|
|
265
|
+
dependencies: SlopcameraCliDependencies,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
const [command, ...rest] = args
|
|
268
|
+
if (command !== "init" && command !== "check" && command !== "render") {
|
|
269
|
+
throw new Error("Use slopcamera diagram sheets init, check, or render <file.drawing.json>")
|
|
270
|
+
}
|
|
271
|
+
const positionals: string[] = []
|
|
272
|
+
const seen = new Set<string>()
|
|
273
|
+
let outDirectory: string | undefined
|
|
274
|
+
let json = false
|
|
275
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
276
|
+
const argument = rest[index]!
|
|
277
|
+
if (!argument.startsWith("-")) {
|
|
278
|
+
positionals.push(argument)
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
if (argument !== "--json" && !(command === "render" && argument === "--out-dir")) {
|
|
282
|
+
throw new Error(`Unknown diagram sheets ${command} option: ${argument}`)
|
|
283
|
+
}
|
|
284
|
+
if (seen.has(argument)) throw new Error(`${argument} may be supplied at most once`)
|
|
285
|
+
seen.add(argument)
|
|
286
|
+
if (argument === "--json") {
|
|
287
|
+
json = true
|
|
288
|
+
} else {
|
|
289
|
+
const value = rest[index + 1]
|
|
290
|
+
if (value === undefined || value.startsWith("-") || value.trim().length === 0) {
|
|
291
|
+
throw new Error("--out-dir requires a directory")
|
|
292
|
+
}
|
|
293
|
+
outDirectory = value
|
|
294
|
+
index += 1
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (positionals.length !== 1 || positionals[0]!.trim().length === 0) {
|
|
298
|
+
throw new Error(`slopcamera diagram sheets ${command} requires exactly one drawing file`)
|
|
299
|
+
}
|
|
300
|
+
const filePath = resolve(positionals[0]!)
|
|
301
|
+
if (!filePath.endsWith(".drawing.json")) throw new Error("Drawing source must end in .drawing.json")
|
|
302
|
+
const log = dependencies.log ?? console.log
|
|
303
|
+
if (command === "init") {
|
|
304
|
+
try {
|
|
305
|
+
await writeFile(filePath, `${JSON.stringify(starterDrawingSource(), null, 2)}\n`, { flag: "wx" })
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") {
|
|
308
|
+
throw new Error(`Refusing to overwrite existing file: ${filePath}`)
|
|
309
|
+
}
|
|
310
|
+
throw error
|
|
311
|
+
}
|
|
312
|
+
log(json ? JSON.stringify({ command: "diagram.sheets.init", source: filePath }) : `Created ${filePath}`)
|
|
313
|
+
reportUsefulResult(dependencies.onUsefulResult)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
if (command === "check") {
|
|
317
|
+
const checks = await withSlopcameraOperationHostAdmission(
|
|
318
|
+
"slopcamera.diagram.check",
|
|
319
|
+
async () => await checkDrawingFile({ filePath }),
|
|
320
|
+
hostAdmissionOptions(dependencies),
|
|
321
|
+
)
|
|
322
|
+
log(json
|
|
323
|
+
? JSON.stringify({ command: "diagram.sheets.check", source: filePath, ...checks })
|
|
324
|
+
: `Valid drawing: ${checks.sheetCount} sheet${checks.sheetCount === 1 ? "" : "s"} (${checks.paper}, ${checks.profile}).`)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
const result = await withSlopcameraOperationHostAdmission(
|
|
328
|
+
"slopcamera.diagram.render",
|
|
329
|
+
async () => await renderDrawingFile({ filePath, ...(outDirectory === undefined ? {} : { outDirectory }) }),
|
|
330
|
+
hostAdmissionOptions(dependencies),
|
|
331
|
+
)
|
|
332
|
+
log(json
|
|
333
|
+
? JSON.stringify({ command: "diagram.sheets.render", artifacts: result.artifacts, checks: result.checks })
|
|
334
|
+
: [result.artifacts.pdf, ...result.artifacts.sheets, result.artifacts.receipt].join("\n"))
|
|
335
|
+
reportUsefulResult(dependencies.onUsefulResult)
|
|
336
|
+
}
|
|
337
|
+
|
|
254
338
|
function canonicalArguments(args: readonly string[]): readonly string[] {
|
|
255
339
|
const [surface, subcommand, ...rest] = args
|
|
256
340
|
if (surface === "diagram") {
|
|
257
341
|
if (subcommand === "init" || subcommand === "check" || subcommand === "render") {
|
|
258
342
|
return [subcommand, ...rest]
|
|
259
343
|
}
|
|
260
|
-
throw new Error("Use slopcamera diagram init, check, or
|
|
344
|
+
throw new Error("Use slopcamera diagram init, check, render, or sheets")
|
|
261
345
|
}
|
|
262
346
|
if (surface === "image") {
|
|
263
347
|
if (
|
|
@@ -286,6 +370,10 @@ export async function main(
|
|
|
286
370
|
args: readonly string[],
|
|
287
371
|
dependencies: SlopcameraCliDependencies = {},
|
|
288
372
|
): Promise<void> {
|
|
373
|
+
if (args[0] === "diagram" && args[1] === "sheets") {
|
|
374
|
+
await runDrawingSheets(args.slice(2), dependencies)
|
|
375
|
+
return
|
|
376
|
+
}
|
|
289
377
|
if (args[0] === "support") {
|
|
290
378
|
process.exitCode = await runProductSupportCommand(args.slice(1),
|
|
291
379
|
dependencies.supportEnvironment === undefined ? {} : { env: dependencies.supportEnvironment })
|