945 lines
35 KiB
JavaScript
945 lines
35 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import {
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { cp } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { gzipSync } from "node:zlib";
|
|
import { cacheBustZooModelingAppAssets } from "./cache-bust-zoo-modeling-app-assets.mjs";
|
|
|
|
const repoUrl = "https://github.com/KittyCAD/modeling-app.git";
|
|
const repoRef =
|
|
process.env.ZOO_MODELING_APP_REF ||
|
|
"f2b345dbb56a851e10d8a742732ab73ee139f025";
|
|
const repoTarballUrls = [
|
|
`https://ghfast.top/https://github.com/KittyCAD/modeling-app/archive/${repoRef}.tar.gz`,
|
|
`https://github.com/KittyCAD/modeling-app/archive/${repoRef}.tar.gz`,
|
|
];
|
|
const root = process.cwd();
|
|
const workspaceDir = path.join(root, ".zoo-modeling-app");
|
|
const sourceDir = path.join(workspaceDir, "modeling-app");
|
|
const sourceRefFile = path.join(sourceDir, ".hicad-source-ref");
|
|
const outputDir = path.join(root, "public/vendor/zoo-modeling-app");
|
|
const customizationDir = path.join(root, "vendor/zoo-modeling-app");
|
|
const proxyToken = "hicad-session";
|
|
const brandedFaviconUrl = "/api/favicon?surface=hicad";
|
|
const enableManufacturingExtension =
|
|
process.env.HICAD_MANUFACTURING_EXTENSION === "1";
|
|
|
|
function run(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: options.cwd || root,
|
|
env: { ...process.env, ...(options.env || {}) },
|
|
shell: false,
|
|
stdio: "inherit",
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(`${command} ${args.join(" ")} failed`);
|
|
}
|
|
}
|
|
|
|
function tryRun(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: options.cwd || root,
|
|
env: { ...process.env, ...(options.env || {}) },
|
|
shell: false,
|
|
stdio: "inherit",
|
|
});
|
|
return result.status === 0;
|
|
}
|
|
|
|
function downloadOfficialSource() {
|
|
mkdirSync(workspaceDir, { recursive: true });
|
|
const archivePath = path.join(workspaceDir, "modeling-app-main.tar.gz");
|
|
const extractedDir = path.join(workspaceDir, `modeling-app-${repoRef}`);
|
|
rmSync(archivePath, { force: true });
|
|
rmSync(extractedDir, { force: true, recursive: true });
|
|
|
|
const downloaded = repoTarballUrls.some((url) =>
|
|
tryRun("curl", [
|
|
"--http1.1",
|
|
"--location",
|
|
"--fail",
|
|
"--retry",
|
|
"3",
|
|
url,
|
|
"--output",
|
|
archivePath,
|
|
])
|
|
);
|
|
if (!downloaded) {
|
|
throw new Error("Unable to download KittyCAD/modeling-app");
|
|
}
|
|
|
|
run("tar", ["-xzf", archivePath, "-C", workspaceDir]);
|
|
rmSync(sourceDir, { force: true, recursive: true });
|
|
run("mv", [extractedDir, sourceDir]);
|
|
writeFileSync(sourceRefFile, `${repoRef}\n`);
|
|
}
|
|
|
|
function ensureOfficialSource() {
|
|
if (existsSync(path.join(sourceDir, "package.json"))) {
|
|
if (!existsSync(sourceRefFile)) {
|
|
writeFileSync(sourceRefFile, `${repoRef}\n`);
|
|
}
|
|
const currentRef = readFileSync(sourceRefFile, "utf8").trim();
|
|
if (currentRef !== repoRef) {
|
|
downloadOfficialSource();
|
|
}
|
|
return;
|
|
}
|
|
downloadOfficialSource();
|
|
}
|
|
|
|
function replaceSource(relativePath, before, after) {
|
|
const filePath = path.join(sourceDir, relativePath);
|
|
const source = readFileSync(filePath, "utf8");
|
|
if (after && source.includes(after)) {
|
|
return;
|
|
}
|
|
if (!source.includes(before)) {
|
|
if (!after) {
|
|
return;
|
|
}
|
|
throw new Error(
|
|
`Official source changed; patch failed for ${relativePath}; expected ${JSON.stringify(
|
|
before.slice(0, 160)
|
|
)}`
|
|
);
|
|
}
|
|
writeFileSync(filePath, source.replace(before, after));
|
|
}
|
|
|
|
function patchOfficialManifest() {
|
|
const manifestPath = path.join(sourceDir, "public/manifest.json");
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
|
|
manifest.short_name = "HiCAD";
|
|
manifest.name = "HiCAD";
|
|
manifest.icons = [
|
|
{
|
|
src: brandedFaviconUrl,
|
|
sizes: "any",
|
|
},
|
|
];
|
|
|
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
}
|
|
|
|
function patchOfficialSource() {
|
|
copyFileSync(
|
|
path.join(customizationDir, "Logo.tsx"),
|
|
path.join(sourceDir, "src/components/Logo.tsx")
|
|
);
|
|
if (enableManufacturingExtension) {
|
|
copyFileSync(
|
|
path.join(customizationDir, "manufacturing.ts"),
|
|
path.join(sourceDir, "src/components/manufacturing.ts")
|
|
);
|
|
copyFileSync(
|
|
path.join(customizationDir, "HicadManufacturingPanel.tsx.template"),
|
|
path.join(sourceDir, "src/components/HicadManufacturingPanel.tsx")
|
|
);
|
|
copyFileSync(
|
|
path.join(customizationDir, "HicadManufacturingLauncher.tsx.template"),
|
|
path.join(sourceDir, "src/components/HicadManufacturingLauncher.tsx")
|
|
);
|
|
copyFileSync(
|
|
path.join(customizationDir, "HicadManufacturingSlot.tsx.template"),
|
|
path.join(sourceDir, "src/components/HicadManufacturingSlot.tsx")
|
|
);
|
|
}
|
|
copyFileSync(
|
|
path.join(customizationDir, "localization.ts"),
|
|
path.join(sourceDir, "src/lib/hicadLocalization.ts")
|
|
);
|
|
copyFileSync(
|
|
path.join(customizationDir, "FitAllButton.tsx.template"),
|
|
path.join(sourceDir, "src/components/FitAllButton.tsx")
|
|
);
|
|
copyFileSync(
|
|
path.join(customizationDir, "HicadProjectExperience.tsx.template"),
|
|
path.join(sourceDir, "src/components/HicadProjectExperience.tsx")
|
|
);
|
|
replaceSource(
|
|
"index.html",
|
|
' <meta name="description" content="An open-source CAD modeling tool from the future by Zoo." />',
|
|
' <meta name="description" content="HiCAD browser-based CAD workspace." />'
|
|
);
|
|
replaceSource(
|
|
"index.html",
|
|
' <link rel="icon" href="/favicon.ico" />',
|
|
` <link rel="icon" href="${brandedFaviconUrl}" />`
|
|
);
|
|
replaceSource(
|
|
"index.html",
|
|
' <link rel="apple-touch-icon" href="/logo192.png" />',
|
|
` <link rel="apple-touch-icon" href="${brandedFaviconUrl}" />`
|
|
);
|
|
patchOfficialManifest();
|
|
replaceSource(
|
|
"index.html",
|
|
' <script defer data-domain="app.zoo.dev" src="https://plausible.corp.zoo.dev/js/script.tagged-events.js"></script>\n',
|
|
""
|
|
);
|
|
replaceSource(
|
|
"index.html",
|
|
" <title>Zoo Design Studio</title>",
|
|
" <title>HiCAD</title>"
|
|
);
|
|
replaceSource(
|
|
"index.html",
|
|
' <meta name="theme-color" content="#000000" />',
|
|
` <meta name="theme-color" content="#000000" />
|
|
<link rel="preload" href="./kcl_wasm_lib_bg.wasm" as="fetch" type="application/wasm" crossorigin="use-credentials" />`
|
|
);
|
|
replaceSource(
|
|
"src/Router.tsx",
|
|
" createBrowserRouter,\n createHashRouter,",
|
|
" createHashRouter,"
|
|
);
|
|
replaceSource(
|
|
"src/Router.tsx",
|
|
"const createRouter = isDesktop() ? createHashRouter : createBrowserRouter",
|
|
"const createRouter = createHashRouter"
|
|
);
|
|
replaceSource(
|
|
"src/Router.tsx",
|
|
"import { isDesktop } from '@src/lib/isDesktop'\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
"import { HicadManufacturingPanel } from '@src/components/HicadManufacturingPanel'\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
" <HicadManufacturingPanel projectName={projectName ?? 'HiCAD 模型'} />\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
"import { HicadManufacturingLauncher } from '@src/components/HicadManufacturingLauncher'\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
" <HicadManufacturingLauncher projectName={projectName ?? 'HiCAD 模型'} />\n",
|
|
""
|
|
);
|
|
if (enableManufacturingExtension) {
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
"import { AppHeader } from '@src/components/AppHeader'\n",
|
|
"import { AppHeader } from '@src/components/AppHeader'\nimport { HicadManufacturingSlot } from '@src/components/HicadManufacturingSlot'\n"
|
|
);
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
" <StatusBar\n",
|
|
" <HicadManufacturingSlot projectName={projectName ?? 'HiCAD 模型'} />\n <StatusBar\n"
|
|
);
|
|
} else {
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
"import { HicadManufacturingSlot } from '@src/components/HicadManufacturingSlot'\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
" <HicadManufacturingSlot projectName={projectName ?? 'HiCAD 模型'} />\n",
|
|
""
|
|
);
|
|
}
|
|
replaceSource(
|
|
"src/lib/socket.ts",
|
|
` ws.addEventListener('open', () => {\n ws.send(\n JSON.stringify({\n type: 'headers',\n headers: {\n Authorization: 'Bearer ' + token,\n },\n })\n )\n resolve(ws)\n })`,
|
|
` ws.addEventListener('open', () => {\n if (token !== '${proxyToken}') {\n ws.send(\n JSON.stringify({\n type: 'headers',\n headers: {\n Authorization: 'Bearer ' + token,\n },\n })\n )\n }\n resolve(ws)\n })`
|
|
);
|
|
replaceSource(
|
|
"src/lib/engineConnection/websocketConnection.ts",
|
|
" if (token) {",
|
|
` if (token && token !== '${proxyToken}') {`
|
|
);
|
|
replaceSource(
|
|
"src/lib/withBaseURL.ts",
|
|
`export function withKittycadWebSocketURL(qs: string): string {\n return mergeQueryString(env().VITE_KITTYCAD_WEBSOCKET_URL, qs)\n}\n\nexport function withMlephantWebSocketURL(qs: string): string {\n return mergeQueryString(env().VITE_MLEPHANT_WEBSOCKET_URL, qs)\n}`,
|
|
`export function withKittycadWebSocketURL(qs: string): string {\n return mergeWebSocketQueryString(env().VITE_KITTYCAD_WEBSOCKET_URL, qs)\n}\n\nexport function withMlephantWebSocketURL(qs: string): string {\n return mergeWebSocketQueryString(env().VITE_MLEPHANT_WEBSOCKET_URL, qs)\n}\n\nfunction mergeWebSocketQueryString(\n url: string | undefined,\n qs: string\n): string {\n if (!url) return ''\n const absoluteUrl =\n typeof window !== 'undefined' && url.startsWith('/')\n ? \`\${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//\${window.location.host}\${url}\`\n : url\n return mergeQueryString(absoluteUrl, qs)\n}`
|
|
);
|
|
replaceSource(
|
|
"src/lang/wasmUtils.ts",
|
|
`export const wasmUrl = () => {\n const wasmFile = '/kcl_wasm_lib_bg.wasm'\n // Check for test environment override\n if (typeof process !== 'undefined' && process.env?.VITE_WASM_BASE_URL) {\n return process.env.VITE_WASM_BASE_URL + wasmFile\n }\n\n // For when we're in electron (file based) or web server (network based)\n // For some reason relative paths don't work as expected. Otherwise we would\n // just do /wasm_lib_bg.wasm. In particular, the issue arises when the path\n // is used from within worker.ts.\n const fullUrl = document.location.protocol.includes('http')\n ? document.location.origin + '/kcl_wasm_lib_bg.wasm'\n : document.location.protocol +\n webSafeJoin(webSafePathSplit(document.location.pathname).slice(0, -1)) +\n wasmFile\n\n return fullUrl\n}`,
|
|
`export const wasmUrl = () => {\n const wasmFile = 'kcl_wasm_lib_bg.wasm'\n if (typeof process !== 'undefined' && process.env?.VITE_WASM_BASE_URL) {\n return process.env.VITE_WASM_BASE_URL + '/' + wasmFile\n }\n\n return new URL(wasmFile, document.baseURI).toString()\n}`
|
|
);
|
|
replaceSource(
|
|
"src/lang/wasmUtils.ts",
|
|
"import { webSafeJoin, webSafePathSplit } from '@src/lib/paths'\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/UserSidebarMenu.tsx",
|
|
" const displayedName = getDisplayName(user)\n",
|
|
` const isHiCadEmbedded = env().VITE_ZOO_API_TOKEN === '${proxyToken}'\n const displayedName = isHiCadEmbedded ? 'HiCAD' : getDisplayName(user)\n`
|
|
);
|
|
replaceSource(
|
|
"src/components/UserSidebarMenu.tsx",
|
|
` [\n {\n id: 'account',`,
|
|
` [\n {\n id: 'hicad-home',\n Element: 'button',\n children: 'Back to HiCAD',\n onClick: () => window.top?.location.assign('/'),\n className: isHiCadEmbedded ? '' : 'hidden',\n },\n {\n id: 'account',`
|
|
);
|
|
replaceSource(
|
|
"src/components/UserSidebarMenu.tsx",
|
|
` ),\n },\n {\n id: 'settings',`,
|
|
` ),\n className: isHiCadEmbedded ? 'hidden' : '',\n },\n {\n id: 'settings',`
|
|
);
|
|
replaceSource(
|
|
"src/components/UserSidebarMenu.tsx",
|
|
" className: '',",
|
|
" className: isHiCadEmbedded ? 'hidden' : '',"
|
|
);
|
|
replaceSource(
|
|
"src/components/UserSidebarMenu.tsx",
|
|
" {user?.image && !imageLoadFailed ? (",
|
|
" {!isHiCadEmbedded && user?.image && !imageLoadFailed ? ("
|
|
);
|
|
replaceSource(
|
|
"src/components/UserSidebarMenu.tsx",
|
|
" {displayedName !== user.email && (",
|
|
" {!isHiCadEmbedded && displayedName !== user.email && ("
|
|
);
|
|
replaceSource(
|
|
"src/components/AppHeader.tsx",
|
|
"import { HicadAutosaveStatus } from '@src/components/HicadProjectExperience'\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/AppHeader.tsx",
|
|
"import { HicadAutosaveStatus, HicadEditorProjectActions } from '@src/components/HicadProjectExperience'\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/AppHeader.tsx",
|
|
"import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'\n",
|
|
"import { HicadAutosaveStatus, HicadEditorProjectActions } from '@src/components/HicadProjectExperience'\nimport ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'\n"
|
|
);
|
|
replaceSource(
|
|
"src/components/AppHeader.tsx",
|
|
" {project && <HicadEditorProjectActions />}\n",
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/AppHeader.tsx",
|
|
` {project && (
|
|
<HicadEditorProjectActions
|
|
onOpenProjectList={() => {
|
|
lsp.onProjectClose(file || null, project.path || null, false)
|
|
kclManager.switchedFiles = true
|
|
void navigate(PATHS.HOME)
|
|
}}
|
|
onCreateBlankProject={() => {
|
|
lsp.onProjectClose(file || null, project.path || null, false)
|
|
kclManager.switchedFiles = true
|
|
void navigate(\`\${PATHS.HOME}?create=blank\`)
|
|
}}
|
|
/>
|
|
)}
|
|
`,
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/AppHeader.tsx",
|
|
` {project && (
|
|
<HicadEditorProjectActions
|
|
onOpenProjectList={() => {
|
|
lsp.onProjectClose(file || null, project.path || null, false)
|
|
kclManager.switchedFiles = true
|
|
}}
|
|
onCreateBlankProject={() => {
|
|
lsp.onProjectClose(file || null, project.path || null, false)
|
|
kclManager.switchedFiles = true
|
|
}}
|
|
/>
|
|
)}
|
|
`,
|
|
""
|
|
);
|
|
replaceSource(
|
|
"src/components/AppHeader.tsx",
|
|
` </ProjectSidebarMenu>
|
|
<div className="flex items-center gap-2 py-1.5 ml-auto">`,
|
|
` </ProjectSidebarMenu>
|
|
{project && (
|
|
<HicadEditorProjectActions
|
|
onOpenProjectList={() => {
|
|
lsp.onProjectClose(file || null, project.path || null, false)
|
|
kclManager.switchedFiles = true
|
|
}}
|
|
onCreateBlankProject={() => {
|
|
lsp.onProjectClose(file || null, project.path || null, false)
|
|
kclManager.switchedFiles = true
|
|
}}
|
|
/>
|
|
)}
|
|
<div className="flex items-center gap-2 py-1.5 ml-auto">`
|
|
);
|
|
replaceSource(
|
|
"src/lib/routeLoaders.ts",
|
|
" if (await webHomeRouteEnabled(app)) {\n",
|
|
" if (true) {\n"
|
|
);
|
|
replaceSource(
|
|
"src/lib/routeLoaders.ts",
|
|
" if (!window.electron && !(await webHomeRouteEnabled(app))) {\n",
|
|
" if (false) {\n"
|
|
);
|
|
replaceSource(
|
|
"src/lib/app.ts",
|
|
` const enableProjectDirectoryCommands =
|
|
typeof window !== 'undefined' &&
|
|
(Boolean(window.electron) ||
|
|
userFeaturesContextHas(
|
|
this.userFeatures.actor.getSnapshot().context,
|
|
OPFS_CLOUD_FEATURE_FLAG,
|
|
false
|
|
))
|
|
`,
|
|
` const enableProjectDirectoryCommands =
|
|
typeof window !== 'undefined'
|
|
`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppHeader.tsx",
|
|
` <div className="flex items-center gap-2 py-1.5 ml-auto">
|
|
{children ||`,
|
|
` <div className="flex items-center gap-2 py-1.5 ml-auto">
|
|
{project && <HicadAutosaveStatus />}
|
|
{children ||`
|
|
);
|
|
replaceSource(
|
|
"src/routes/Home.tsx",
|
|
"import AppProjectCard from '@src/components/AppProjectCard/AppProjectCard'\n",
|
|
"import AppProjectCard from '@src/components/AppProjectCard/AppProjectCard'\nimport { HicadProjectDashboardHeading } from '@src/components/HicadProjectExperience'\n"
|
|
);
|
|
replaceSource(
|
|
"src/routes/Home.tsx",
|
|
` projectSearchKeybinding={projectSearchKeybinding}
|
|
className="col-start-2 -col-end-1"`,
|
|
` projectSearchKeybinding={projectSearchKeybinding}
|
|
projectCount={scopedHomeProjectEntries.length}
|
|
className="col-start-2 -col-end-1"`
|
|
);
|
|
replaceSource(
|
|
"src/routes/Home.tsx",
|
|
` projectSearchKeybinding?: string
|
|
}`,
|
|
` projectSearchKeybinding?: string
|
|
projectCount: number
|
|
}`
|
|
);
|
|
replaceSource(
|
|
"src/routes/Home.tsx",
|
|
` projectSearchKeybinding,
|
|
...rest`,
|
|
` projectSearchKeybinding,
|
|
projectCount,
|
|
...rest`
|
|
);
|
|
replaceSource(
|
|
"src/routes/Home.tsx",
|
|
` <div className="flex gap-8 items-center">
|
|
<div className="flex flex-col gap-1">
|
|
{library && showLibraryBackLink && (
|
|
<Link
|
|
to={PATHS.HOME}
|
|
className="text-sm text-chalkboard-70 underline underline-offset-2 dark:text-chalkboard-30"
|
|
>
|
|
All libraries
|
|
</Link>
|
|
)}
|
|
<h1 className="text-3xl font-bold">{title}</h1>
|
|
</div>
|
|
</div>`,
|
|
` <div className="flex min-w-0 flex-1 gap-8 items-center">
|
|
{library && showLibraryBackLink && (
|
|
<Link
|
|
to={PATHS.HOME}
|
|
className="text-sm text-chalkboard-70 underline underline-offset-2 dark:text-chalkboard-30"
|
|
>
|
|
返回全部项目
|
|
</Link>
|
|
)}
|
|
<HicadProjectDashboardHeading projectCount={projectCount} />
|
|
</div>`
|
|
);
|
|
replaceSource(
|
|
"src/routes/Home.tsx",
|
|
`<div className="flex flex-col md:flex-row gap-4 justify-between md:items-center select-none">`,
|
|
`<div className="flex flex-col gap-5 justify-between select-none">`
|
|
);
|
|
replaceSource(
|
|
"src/routes/Home.tsx",
|
|
`<div className="flex flex-col sm:flex-row gap-2 sm:items-center">`,
|
|
`<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
"import { ContextMenu, ContextMenuItem } from '@src/components/ContextMenu'\n",
|
|
"import { ContextMenu, ContextMenuItem } from '@src/components/ContextMenu'\nimport { CustomIcon } from '@src/components/CustomIcon'\n"
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
` {
|
|
local: 'Local',
|
|
'cloud-only': 'Cloud-only',
|
|
syncing: 'Syncing',
|
|
synced: 'Synced',
|
|
conflicted: 'Conflicted',
|
|
}`,
|
|
` {
|
|
local: '已保存',
|
|
'cloud-only': '云端项目',
|
|
syncing: '保存中',
|
|
synced: '已保存',
|
|
conflicted: '需要处理',
|
|
}`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
`function getDisplayedTime(dateTimeMs: number) {
|
|
const date = new Date(dateTimeMs)
|
|
const startOfToday = new Date()
|
|
startOfToday.setHours(0, 0, 0, 0)
|
|
return date.getTime() < startOfToday.getTime()
|
|
? date.toLocaleDateString()
|
|
: date.toLocaleTimeString()
|
|
}`,
|
|
`function getDisplayedTime(dateTimeMs: number) {
|
|
const date = new Date(dateTimeMs)
|
|
const now = new Date()
|
|
const isToday =
|
|
date.getFullYear() === now.getFullYear() &&
|
|
date.getMonth() === now.getMonth() &&
|
|
date.getDate() === now.getDate()
|
|
|
|
if (isToday) {
|
|
return \`今天 \${date.toLocaleTimeString('zh-CN', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
})}\`
|
|
}
|
|
|
|
return date.toLocaleDateString('zh-CN', {
|
|
year: date.getFullYear() === now.getFullYear() ? undefined : 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
})
|
|
}`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
` {project.kclFileCount !== undefined && (
|
|
<span className="px-2 text-chalkboard-60 text-xs">
|
|
<span data-testid="project-file-count">{project.kclFileCount}</span>{' '}
|
|
file{project.kclFileCount === 1 ? '' : 's'}{' '}
|
|
{(project.directoryCount ?? 0) > 0 && (
|
|
<>
|
|
{'/ '}
|
|
<span data-testid="project-folder-count">
|
|
{project.directoryCount}
|
|
</span>{' '}
|
|
folder{project.directoryCount === 1 ? '' : 's'}
|
|
</>
|
|
)}
|
|
</span>
|
|
)}
|
|
<span className="px-2 text-chalkboard-60 text-xs">
|
|
Edited{' '}
|
|
<span data-testid="project-edit-date">
|
|
{displayedProject.modified
|
|
? getDisplayedTime(displayedProject.modified)
|
|
: 'never'}
|
|
</span>
|
|
</span>`,
|
|
` <div className="flex items-center justify-between gap-2 px-2 text-xs text-chalkboard-60">
|
|
<span>
|
|
{project.kclFileCount !== undefined ? (
|
|
<>
|
|
<span data-testid="project-file-count">{project.kclFileCount}</span>{' '}
|
|
个模型文件
|
|
</>
|
|
) : (
|
|
'CAD 项目'
|
|
)}
|
|
</span>
|
|
<span className="truncate">
|
|
最近编辑{' '}
|
|
<span data-testid="project-edit-date">
|
|
{displayedProject.modified
|
|
? getDisplayedTime(displayedProject.modified)
|
|
: '暂无记录'}
|
|
</span>
|
|
</span>
|
|
</div>`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
` title="Delete Project"`,
|
|
` title="删除项目"`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
` <p className="my-4 text-wrap break-words">
|
|
This will permanently delete "{projectName || 'this file'}
|
|
".
|
|
</p>
|
|
<p className="my-4 text-wrap break-words">
|
|
Are you sure you want to delete "{projectName || 'this file'}
|
|
"? This action cannot be undone.
|
|
</p>`,
|
|
` <p className="my-4 text-wrap break-words">
|
|
确定删除“{projectName || '此项目'}”吗?
|
|
</p>
|
|
<p className="my-4 text-wrap break-words text-sm text-chalkboard-70 dark:text-chalkboard-30">
|
|
项目及其全部模型文件将被永久删除,此操作无法撤销。
|
|
</p>`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
` title={projectName}
|
|
titleText={projectName}`,
|
|
` title={
|
|
<span className="flex min-w-0 items-center justify-between gap-2">
|
|
<span className="truncate font-semibold">{projectName}</span>
|
|
{canOpen && (
|
|
<span className="flex-none text-xs font-medium text-primary opacity-0 transition-opacity group-hover:opacity-100">
|
|
继续设计
|
|
</span>
|
|
)}
|
|
</span>
|
|
}
|
|
titleText={projectName}`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
` thumbnailUrl={imageUrl}
|
|
badges={badges}`,
|
|
` thumbnailUrl={imageUrl}
|
|
thumbnailAlt={projectName ? \`\${projectName} 的模型预览\` : 'CAD 项目预览'}
|
|
thumbnailFallback={
|
|
<div className="flex h-full w-full flex-col items-center justify-center gap-2 text-chalkboard-50 dark:text-chalkboard-50">
|
|
<CustomIcon name="model" className="h-8 w-8" />
|
|
<span className="text-xs">暂无模型预览</span>
|
|
</div>
|
|
}
|
|
badges={badges}`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
` Rename project`,
|
|
` 重命名`
|
|
);
|
|
replaceSource(
|
|
"src/components/AppProjectCard/AppProjectCard.tsx",
|
|
` Delete project`,
|
|
` 删除项目`
|
|
);
|
|
replaceSource(
|
|
"src/lib/commandBarConfigs/projectsCommandConfig.ts",
|
|
` displayName: \`Create project\`,
|
|
description: 'Create a project',`,
|
|
` displayName: \`新建 CAD 项目\`,
|
|
description: '输入项目名称即可开始,设计内容会自动保存。',`
|
|
);
|
|
replaceSource(
|
|
"src/lib/commandBarConfigs/projectsCommandConfig.ts",
|
|
` displayName: 'Title',
|
|
required: true,`,
|
|
` displayName: '项目名称',
|
|
description: '使用容易识别的名称,例如“五金连接安装板”。',
|
|
required: true,`
|
|
);
|
|
replaceSource(
|
|
"src/lib/commandBarConfigs/projectsCommandConfig.ts",
|
|
` displayName: 'Library',
|
|
required: () => createProjectLibraryOptions().length > 1,`,
|
|
` displayName: '保存位置',
|
|
required: () => createProjectLibraryOptions().length > 1,`
|
|
);
|
|
replaceSource(
|
|
"src/lib/commandBarConfigs/projectsCommandConfig.ts",
|
|
` defaultValue: defaultProjectFolderNameSnapshot,`,
|
|
` defaultValue: () => '',`
|
|
);
|
|
replaceSource(
|
|
"src/components/CommandBar/CommandBarBasicInput.tsx",
|
|
` placeholder="Enter a value"`,
|
|
` placeholder={
|
|
commandBarState.context.selectedCommand?.name === 'Create project'
|
|
? '例如:五金连接安装板'
|
|
: '请输入'
|
|
}`
|
|
);
|
|
replaceSource(
|
|
"src/components/environment/Environment.tsx",
|
|
" new URL(env().VITE_KITTYCAD_WEBSOCKET_URL ?? ''),",
|
|
" new URL(env().VITE_KITTYCAD_WEBSOCKET_URL ?? '', window.location.href),"
|
|
);
|
|
replaceSource(
|
|
"src/components/environment/Environment.tsx",
|
|
" new URL(env().VITE_MLEPHANT_WEBSOCKET_URL ?? '')",
|
|
" new URL(env().VITE_MLEPHANT_WEBSOCKET_URL ?? '', window.location.href)"
|
|
);
|
|
replaceSource(
|
|
"src/components/ConnectionStream.tsx",
|
|
` className="cursor-pointer"\n id="freeze-frame"`,
|
|
` className="absolute inset-0 h-full w-full cursor-pointer"\n id="freeze-frame"`
|
|
);
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
"import { getMlEphantProjectReloadBehavior } from '@src/components/openedProjectUtils'\n",
|
|
"import { getMlEphantProjectReloadBehavior } from '@src/components/openedProjectUtils'\nimport env from '@src/env'\n"
|
|
);
|
|
replaceSource(
|
|
"src/components/OpenedProject.tsx",
|
|
` const needsOnboarded =
|
|
!window.electron &&`,
|
|
` const needsOnboarded =
|
|
env().VITE_ZOO_API_TOKEN !== '${proxyToken}' &&
|
|
!window.electron &&`
|
|
);
|
|
replaceSource(
|
|
"src/index.tsx",
|
|
"import { markOnce } from '@src/lib/performance'\n",
|
|
"import { installHiCadLocalization } from '@src/lib/hicadLocalization'\nimport { markOnce } from '@src/lib/performance'\n"
|
|
);
|
|
replaceSource(
|
|
"src/index.tsx",
|
|
"function launchApp(app: App) {\n initSingletonBehavior(app)",
|
|
"function launchApp(app: App) {\n installHiCadLocalization()\n initSingletonBehavior(app)"
|
|
);
|
|
replaceSource(
|
|
"src/registry/extensions/engineScene/viewExtensionControls.tsx",
|
|
"import { CustomIcon } from '@src/components/CustomIcon'\n",
|
|
"import { CustomIcon } from '@src/components/CustomIcon'\nimport { FitAllButton } from '@src/components/FitAllButton'\n"
|
|
);
|
|
replaceSource(
|
|
"src/registry/extensions/engineScene/viewExtensionControls.tsx",
|
|
` <Suspense fallback={null}>\n <Gizmo />\n </Suspense>`,
|
|
` <Suspense fallback={null}>\n <div className="flex items-end gap-2">\n <FitAllButton />\n <Gizmo />\n </div>\n </Suspense>`
|
|
);
|
|
replaceSource(
|
|
"src/lib/resetCameraPosition.ts",
|
|
` // We need a padding of 0.1 for zoom_to_fit for all E2E tests since they were originally\n // written with zoom_to_fit with padding 0.1\n const padding = 0.1\n if (isPlaywright()) {`,
|
|
` // Keep the official E2E framing stable while giving HiCAD users enough\n // breathing room to see the complete model after generation and file loads.\n const padding = isPlaywright() ? 0.1 : 0.35\n if (isPlaywright()) {`
|
|
);
|
|
replaceSource(
|
|
"src/lib/resetCameraPosition.ts",
|
|
` await engineViewIsometric({
|
|
engineCommandManager,
|
|
padding,
|
|
})`,
|
|
` await engineViewIsometric({
|
|
engineCommandManager,
|
|
padding,
|
|
})
|
|
|
|
// view_isometric changes the direction but can preserve the previous
|
|
// close-up distance. Refit against every generated entity so newly
|
|
// completed models open fully framed instead of showing a local detail.
|
|
await engineStreamZoomToFit({
|
|
engineCommandManager,
|
|
padding,
|
|
})`
|
|
);
|
|
replaceSource(
|
|
"src/lib/commandBarConfigs/standardViewsConfig.ts",
|
|
"engineStreamZoomToFit({ engineCommandManager, padding: 0.1 })",
|
|
"engineStreamZoomToFit({ engineCommandManager, padding: 0.35 })"
|
|
);
|
|
replaceSource(
|
|
"src/lib/app.ts",
|
|
" padding: 0.2,\n animated: false,",
|
|
" padding: 0.35,\n animated: false,"
|
|
);
|
|
replaceSource(
|
|
"src/clientSideScene/ClientSideSceneComp.tsx",
|
|
"import { throttle, toSync } from '@src/lib/utils'",
|
|
`import {\n engineStreamZoomToFit,\n throttle,\n toSync,\n} from '@src/lib/utils'`
|
|
);
|
|
replaceSource(
|
|
"src/clientSideScene/ClientSideSceneComp.tsx",
|
|
` const {\n kclManager: { sceneEntitiesManager, sceneInfra, engineCommandManager },\n } = useSingletons()`,
|
|
` const { kclManager } = useSingletons()\n const { sceneEntitiesManager, sceneInfra, engineCommandManager } = kclManager`
|
|
);
|
|
replaceSource(
|
|
"src/clientSideScene/ClientSideSceneComp.tsx",
|
|
` // Detect canvas size changes\n const observer = new ResizeObserver(() => {\n // This is called initially too, not just on resize\n sceneInfra.onCanvasResized()\n sceneInfra.camControls.onWindowResize()\n sceneEntitiesManager.onCamChange().catch(reportRejection)\n })\n observer.observe(container)`,
|
|
` // Preserve the full-model framing when panels or the browser resize.\n let fitAfterResizeTimer: ReturnType<typeof setTimeout> | undefined\n const observer = new ResizeObserver(() => {\n // This is called initially too, not just on resize\n sceneInfra.onCanvasResized()\n sceneInfra.camControls.onWindowResize()\n sceneEntitiesManager.onCamChange().catch(reportRejection)\n\n clearTimeout(fitAfterResizeTimer)\n fitAfterResizeTimer = setTimeout(() => {\n if (kclManager.artifactGraph.size === 0) return\n engineStreamZoomToFit({\n engineCommandManager,\n padding: 0.35,\n }).catch(reportRejection)\n }, 180)\n })\n observer.observe(container)`
|
|
);
|
|
replaceSource(
|
|
"src/clientSideScene/ClientSideSceneComp.tsx",
|
|
` observer.disconnect()\n media.removeEventListener('change', handleChange)`,
|
|
` observer.disconnect()\n clearTimeout(fitAfterResizeTimer)\n media.removeEventListener('change', handleChange)`
|
|
);
|
|
replaceSource(
|
|
"src/routes/Onboarding/utils.tsx",
|
|
` <p className="m-0 text-center text-xs italic text-chalkboard-70 dark:text-chalkboard-30">
|
|
To view your account, manage payment methods, or change tiers, simply{' '}`,
|
|
` <p
|
|
data-hicad-official-account-link
|
|
className="m-0 text-center text-xs italic text-chalkboard-70 dark:text-chalkboard-30"
|
|
>
|
|
To view your account, manage payment methods, or change tiers, simply{' '}`
|
|
);
|
|
}
|
|
|
|
function ensureDependencies() {
|
|
if (existsSync(path.join(sourceDir, "node_modules/.package-lock.json"))) {
|
|
return;
|
|
}
|
|
run("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], {
|
|
cwd: sourceDir,
|
|
});
|
|
}
|
|
|
|
function ensureWasm() {
|
|
const wasmJs = path.join(
|
|
sourceDir,
|
|
"rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js"
|
|
);
|
|
const wasmBinary = path.join(
|
|
sourceDir,
|
|
"rust/kcl-wasm-lib/pkg/kcl_wasm_lib_bg.wasm"
|
|
);
|
|
if (!(existsSync(wasmJs) && existsSync(wasmBinary))) {
|
|
const cargoEnv = {
|
|
CARGO_HTTP_MULTIPLEXING: "false",
|
|
CARGO_REGISTRIES_CRATES_IO_INDEX:
|
|
process.env.CARGO_REGISTRIES_CRATES_IO_INDEX ||
|
|
"sparse+https://rsproxy.cn/index/",
|
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: "sparse",
|
|
};
|
|
run(
|
|
"wasm-pack",
|
|
[
|
|
"build",
|
|
"kcl-wasm-lib",
|
|
"--release",
|
|
"--target",
|
|
"web",
|
|
"--out-dir",
|
|
"pkg",
|
|
"--scope",
|
|
"kittycad",
|
|
"--no-opt",
|
|
],
|
|
{ cwd: path.join(sourceDir, "rust"), env: cargoEnv }
|
|
);
|
|
run(
|
|
"cargo",
|
|
[
|
|
"test",
|
|
"-p",
|
|
"kcl-lib",
|
|
"--features",
|
|
"artifact-graph",
|
|
"export_bindings",
|
|
],
|
|
{ cwd: path.join(sourceDir, "rust"), env: cargoEnv }
|
|
);
|
|
}
|
|
|
|
copyFileSync(
|
|
path.join(sourceDir, "rust/kcl-wasm-lib/README.md"),
|
|
path.join(sourceDir, "rust/kcl-wasm-lib/pkg/README.md")
|
|
);
|
|
copyFileSync(wasmBinary, path.join(sourceDir, "public/kcl_wasm_lib_bg.wasm"));
|
|
}
|
|
|
|
function precompressVersionedAssets(assetsDir) {
|
|
for (const entry of readdirSync(assetsDir, { withFileTypes: true })) {
|
|
if (!entry.isFile() || !/\.(?:css|js|wasm)$/.test(entry.name)) {
|
|
continue;
|
|
}
|
|
const assetPath = path.join(assetsDir, entry.name);
|
|
writeFileSync(`${assetPath}.gz`, gzipSync(readFileSync(assetPath), { level: 9 }));
|
|
}
|
|
}
|
|
|
|
function precompressRootWasm(buildDir) {
|
|
const wasmPath = path.join(buildDir, "kcl_wasm_lib_bg.wasm");
|
|
if (!existsSync(wasmPath)) return;
|
|
writeFileSync(
|
|
`${wasmPath}.gz`,
|
|
gzipSync(readFileSync(wasmPath), { level: 9 })
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
ensureOfficialSource();
|
|
ensureDependencies();
|
|
ensureWasm();
|
|
patchOfficialSource();
|
|
|
|
const buildEnv = {
|
|
VITE_KITTYCAD_WEBSOCKET_URL:
|
|
process.env.HICAD_ZOO_MODELING_WEBSOCKET_URL ||
|
|
"/api/ai-cad/zoo-ws/modeling/commands",
|
|
VITE_MLEPHANT_WEBSOCKET_URL:
|
|
process.env.HICAD_ZOO_AGENT_WEBSOCKET_URL ||
|
|
"/api/ai-cad/zoo-ws/ml/copilot",
|
|
VITE_ZOO_API_BASE_URL: "/api/ai-cad/zoo",
|
|
VITE_ZOO_API_TOKEN: proxyToken,
|
|
VITE_ZOO_BASE_DOMAIN: "zoo.dev",
|
|
};
|
|
rmSync(path.join(sourceDir, "build"), { force: true, recursive: true });
|
|
run("npx", ["vite", "build", "--base=./", "--mode=production"], {
|
|
cwd: sourceDir,
|
|
env: buildEnv,
|
|
});
|
|
|
|
const buildDir = path.join(sourceDir, "build");
|
|
if (!existsSync(path.join(buildDir, "index.html"))) {
|
|
throw new Error("Zoo Design Studio build did not produce index.html");
|
|
}
|
|
precompressVersionedAssets(path.join(buildDir, "assets"));
|
|
precompressRootWasm(buildDir);
|
|
cacheBustZooModelingAppAssets(buildDir);
|
|
|
|
rmSync(outputDir, { force: true, recursive: true });
|
|
mkdirSync(path.dirname(outputDir), { recursive: true });
|
|
await cp(buildDir, outputDir, { recursive: true });
|
|
|
|
const license = path.join(sourceDir, "LICENSE");
|
|
if (existsSync(license)) {
|
|
await cp(license, path.join(outputDir, "LICENSE"));
|
|
}
|
|
writeFileSync(
|
|
path.join(outputDir, "SOURCE.txt"),
|
|
[
|
|
"HiCAD web frontend based on Zoo Design Studio",
|
|
`Source: ${repoUrl}`,
|
|
`Source commit: ${repoRef}`,
|
|
"License: MIT",
|
|
"Authentication: HiCAD session with server-side Zoo proxy",
|
|
"",
|
|
].join("\n")
|
|
);
|
|
|
|
console.log(`HiCAD official frontend copied to ${outputDir}`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
});
|