fix: isolate HiCAD manufacturing extension

This commit is contained in:
134 2026-07-27 20:09:12 +08:00
parent 1038adae46
commit 36bcb3f5a5
5 changed files with 179 additions and 45 deletions

View File

@ -142,6 +142,10 @@ function patchOfficialSource() {
path.join(customizationDir, "HicadManufacturingPanel.tsx"),
path.join(sourceDir, "src/components/HicadManufacturingPanel.tsx")
);
copyFileSync(
path.join(customizationDir, "HicadManufacturingLauncher.tsx"),
path.join(sourceDir, "src/components/HicadManufacturingLauncher.tsx")
);
copyFileSync(
path.join(customizationDir, "localization.ts"),
path.join(sourceDir, "src/lib/hicadLocalization.ts")
@ -197,15 +201,25 @@ function patchOfficialSource() {
"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 { AppHeader } from '@src/components/AppHeader'\n",
"import { AppHeader } from '@src/components/AppHeader'\nimport { HicadManufacturingPanel } from '@src/components/HicadManufacturingPanel'\n"
"import { AppHeader } from '@src/components/AppHeader'\nimport { HicadManufacturingLauncher } from '@src/components/HicadManufacturingLauncher'\n"
);
replaceSource(
"src/components/OpenedProject.tsx",
" <StatusBar\n",
" <HicadManufacturingPanel projectName={projectName ?? 'HiCAD 模型'} />\n <StatusBar\n"
" <HicadManufacturingLauncher projectName={projectName ?? 'HiCAD 模型'} />\n <StatusBar\n"
);
replaceSource(
"src/lib/socket.ts",

View File

@ -45,16 +45,25 @@ test.describe('HiCAD manufacturing extension', () => {
)
expect(vendorScript).toContain(
'vendor/zoo-modeling-app/HicadManufacturingPanel.tsx'
'vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx'
.split('/')
.at(-1)
)
expect(vendorScript).toContain(
"import { HicadManufacturingPanel } from '@src/components/HicadManufacturingPanel'"
"import { HicadManufacturingLauncher } from '@src/components/HicadManufacturingLauncher'"
)
expect(vendorScript).toContain(
"<HicadManufacturingPanel projectName={projectName ?? 'HiCAD 模型'} />"
"<HicadManufacturingLauncher projectName={projectName ?? 'HiCAD 模型'} />"
)
const launcher = fs.readFileSync(
path.join(
projectRoot,
'vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx'
),
'utf8'
)
expect(launcher).toContain("lazy(() =>")
expect(launcher).toContain('ManufacturingBoundary')
})
test('creates non-zero reversible offsets for every independent body', () => {

View File

@ -0,0 +1,100 @@
import {
Component,
lazy,
Suspense,
useState,
type ErrorInfo,
type ReactNode,
} from 'react'
const ManufacturingPanel = lazy(() =>
import('./HicadManufacturingPanel').then((module) => ({
default: module.HicadManufacturingPanel,
}))
)
class ManufacturingBoundary extends Component<
{
children: ReactNode
onClose: () => void
},
{ failed: boolean }
> {
state = { failed: false }
static getDerivedStateFromError() {
return { failed: true }
}
componentDidCatch(error: unknown, info: ErrorInfo) {
console.error('[HiCAD Manufacturing] Extension failed safely', error, info)
}
render() {
if (!this.state.failed) return this.props.children
return (
<div
className="fixed inset-0 z-[100] grid place-items-center bg-black/50 p-4"
role="dialog"
aria-modal="true"
aria-label="工程工具加载失败"
>
<div className="w-full max-w-md border border-chalkboard-30 bg-chalkboard-10 p-6 shadow-2xl dark:border-chalkboard-70 dark:bg-chalkboard-100">
<h2 className="text-lg font-semibold">工程工具暂时无法打开</h2>
<p className="mt-2 text-sm text-chalkboard-60 dark:text-chalkboard-40">
HiCAD 主编辑器没有受到影响。关闭后可以继续建模并稍后重试。
</p>
<button
type="button"
className="mt-5 h-9 bg-chalkboard-100 px-4 text-sm font-medium text-chalkboard-10 dark:bg-chalkboard-10 dark:text-chalkboard-100"
onClick={this.props.onClose}
>
关闭
</button>
</div>
</div>
)
}
}
export function HicadManufacturingLauncher({
projectName,
}: {
projectName: string
}) {
const [active, setActive] = useState(false)
return (
<>
<button
type="button"
data-testid="hicad-manufacturing-trigger"
onClick={() => setActive(true)}
className="absolute right-4 top-14 z-20 h-9 border border-chalkboard-30 bg-chalkboard-10 px-3 text-sm font-medium shadow-sm hover:bg-chalkboard-20 dark:border-chalkboard-70 dark:bg-chalkboard-90 dark:hover:bg-chalkboard-80"
>
工程出图
</button>
{active && (
<ManufacturingBoundary onClose={() => setActive(false)}>
<Suspense
fallback={
<div className="fixed inset-0 z-[100] grid place-items-center bg-black/50">
<div className="border border-chalkboard-30 bg-chalkboard-10 px-5 py-4 text-sm shadow-xl dark:border-chalkboard-70 dark:bg-chalkboard-100">
正在加载工程工具…
</div>
</div>
}
>
<ManufacturingPanel
hideTrigger
initialOpen
onRequestClose={() => setActive(false)}
projectName={projectName}
/>
</Suspense>
</ManufacturingBoundary>
)}
</>
)
}

View File

@ -1,5 +1,4 @@
import type { BoundingBox, ModelingCmd } from '@kittycad/lib'
import { useSignals } from '@preact/signals-react/runtime'
import { getBodiesFromArtifactGraph } from '@src/lang/std/artifactGraph'
import { useSingletons } from '@src/lib/boot'
import { isModelingResponse } from '@src/lib/kcSdkGuards'
@ -109,13 +108,18 @@ function dataUrlFromSnapshot(contents: string) {
}
export function HicadManufacturingPanel({
hideTrigger = false,
initialOpen = false,
onRequestClose,
projectName,
}: {
hideTrigger?: boolean
initialOpen?: boolean
onRequestClose?: () => void
projectName: string
}) {
useSignals()
const { kclManager } = useSingletons()
const [open, setOpen] = useState(false)
const [open, setOpen] = useState(initialOpen)
const [busy, setBusy] = useState(false)
const [bodies, setBodies] = useState<BodyMeasurement[]>([])
const [snapshot, setSnapshot] = useState<string>()
@ -131,37 +135,29 @@ export function HicadManufacturingPanel({
const explodeOffsetsRef = useRef<ExplodeOffset[]>([])
const resolvedProjectName = projectName || 'HiCAD 模型'
useEffect(() => {
let active = true
void fetch('/api/site-settings?scope=chat', {
credentials: 'same-origin',
})
.then((response) => {
if (!response.ok) throw new Error('Unable to load site settings')
return response.json() as Promise<Record<string, unknown>>
const loadBrandSettings = async () => {
try {
const response = await fetch('/api/site-settings?scope=chat', {
credentials: 'same-origin',
})
.then((settings) => {
if (!active) return
const name =
typeof settings.siteName === 'string' && settings.siteName.trim()
? settings.siteName.trim()
: '由壹点AI'
const configuredLogo =
typeof settings.siteLogo === 'string' && settings.siteLogo.trim()
? settings.siteLogo.trim()
: typeof settings.siteLogoDark === 'string' &&
settings.siteLogoDark.trim()
? settings.siteLogoDark.trim()
: '/api/favicon?surface=hicad'
setBrand({ name, logoUrl: configuredLogo })
})
.catch(() => {
// Keep the built-in website branding when settings are unavailable.
})
return () => {
active = false
if (!response.ok) return
const settings = (await response.json()) as Record<string, unknown>
const name =
typeof settings.siteName === 'string' && settings.siteName.trim()
? settings.siteName.trim()
: '由壹点AI'
const configuredLogo =
typeof settings.siteLogo === 'string' && settings.siteLogo.trim()
? settings.siteLogo.trim()
: typeof settings.siteLogoDark === 'string' &&
settings.siteLogoDark.trim()
? settings.siteLogoDark.trim()
: '/api/favicon?surface=hicad'
setBrand({ name, logoUrl: configuredLogo })
} catch {
// Keep the built-in website branding when settings are unavailable.
}
}, [])
}
const sendModelingCommand = async (cmd: ModelingCmd) => {
const response = await kclManager.engineCommandManager.sendSceneCommand({
@ -304,8 +300,17 @@ export function HicadManufacturingPanel({
}
}
// This component is lazy-mounted by the launcher, so automatic analysis only
// starts after the user explicitly opens the engineering tools.
useEffect(() => {
if (!initialOpen) return
void loadBrandSettings()
void analyzeModel()
}, [initialOpen])
const handleOpen = () => {
setOpen(true)
void loadBrandSettings()
if (bodies.length === 0) {
void analyzeModel()
}
@ -450,6 +455,7 @@ export function HicadManufacturingPanel({
})
}
setOpen(false)
onRequestClose?.()
} finally {
setBusy(false)
}
@ -457,14 +463,16 @@ export function HicadManufacturingPanel({
return (
<>
<button
type="button"
data-testid="hicad-manufacturing-trigger"
onClick={handleOpen}
className="absolute right-4 top-14 z-20 h-9 px-3 border border-chalkboard-30 dark:border-chalkboard-70 bg-chalkboard-10 dark:bg-chalkboard-90 hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 shadow-sm text-sm font-medium"
>
工程出图
</button>
{!hideTrigger && (
<button
type="button"
data-testid="hicad-manufacturing-trigger"
onClick={handleOpen}
className="absolute right-4 top-14 z-20 h-9 border border-chalkboard-30 bg-chalkboard-10 px-3 text-sm font-medium shadow-sm hover:bg-chalkboard-20 dark:border-chalkboard-70 dark:bg-chalkboard-90 dark:hover:bg-chalkboard-80"
>
工程出图
</button>
)}
{open && (
<div

View File

@ -8,6 +8,9 @@ reviewed official `KittyCAD/modeling-app` source.
- `HicadManufacturingPanel.tsx` integrates engine measurements, reversible
exploded views, BOM editing, CSV export, snapshots, and printable production
sheets.
- `HicadManufacturingLauncher.tsx` lazy-loads the extension only after the user
clicks **工程出图** and contains a dedicated error boundary, so an extension
failure cannot take down the official HiCAD editor.
- `manufacturing.ts` contains the engine-independent geometry, BOM, and report
generation logic.
- `scripts/vendor-zoo-modeling-app.mjs` copies both files into the official