fix: isolate HiCAD manufacturing extension
This commit is contained in:
parent
1038adae46
commit
36bcb3f5a5
@ -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",
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
100
vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx
vendored
Normal file
100
vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx
vendored
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
3
vendor/zoo-modeling-app/README.md
vendored
3
vendor/zoo-modeling-app/README.md
vendored
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user