upadate
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -913,10 +913,18 @@ def getLib():
|
|||||||
@app.route('/api/component/<component_name>')
|
@app.route('/api/component/<component_name>')
|
||||||
@login_required_json
|
@login_required_json
|
||||||
def getComp(component_name):
|
def getComp(component_name):
|
||||||
"""Return component YAML data."""
|
"""Return component YAML data with injected category."""
|
||||||
data = readCompYaml(component_name, pdk_root_for_request_project())
|
comps_root = pdk_root_for_request_project()
|
||||||
|
data = readCompYaml(component_name, comps_root)
|
||||||
if data is None:
|
if data is None:
|
||||||
return jsonify({"error": "Component not found"}), 404
|
return jsonify({"error": "Component not found"}), 404
|
||||||
|
# Derive the category (parent folder name) for icon resolution on the canvas.
|
||||||
|
if isinstance(data, dict) and '__category__' not in data:
|
||||||
|
search_root = comps_root or current_pdk_root()
|
||||||
|
for root, dirs, files in os.walk(search_root):
|
||||||
|
if os.path.basename(root) == component_name:
|
||||||
|
data['__category__'] = os.path.basename(os.path.dirname(root))
|
||||||
|
break
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
@app.route('/api/component/<component_name>/image')
|
@app.route('/api/component/<component_name>/image')
|
||||||
|
|||||||
@@ -12,6 +12,13 @@
|
|||||||
}
|
}
|
||||||
root.MxpicCanvasHelpers = helpers;
|
root.MxpicCanvasHelpers = helpers;
|
||||||
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
||||||
|
// Global origin for YAML coordinate export/import so YAML values always match
|
||||||
|
// the RightPanel display coordinate system.
|
||||||
|
let __exportOriginX = null;
|
||||||
|
let __exportOriginY = null;
|
||||||
|
var setExportOrigin = (x, y) => { __exportOriginX = x; __exportOriginY = y; };
|
||||||
|
var getExportOrigin = () => ({ x: __exportOriginX, y: __exportOriginY });
|
||||||
|
|
||||||
// Label used by the canvas to represent generated mxpic_forge components.
|
// Label used by the canvas to represent generated mxpic_forge components.
|
||||||
const FORGE_COMPONENT_LABEL = 'generate with mxpic_forge';
|
const FORGE_COMPONENT_LABEL = 'generate with mxpic_forge';
|
||||||
// Serialized component type used when saving mxpic_forge-generated components.
|
// Serialized component type used when saving mxpic_forge-generated components.
|
||||||
@@ -651,10 +658,20 @@
|
|||||||
return JSON.stringify(String(value));
|
return JSON.stringify(String(value));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert canvas Y coordinates into layout Y coordinates.
|
// Convert canvas coordinates into layout/display coordinates.
|
||||||
const canvasToLayoutY = (value) => -Number(value || 0);
|
// When origin is set, uses the RightPanel formula: displayY = originY - rfY
|
||||||
// Convert layout Y coordinates back into canvas Y coordinates.
|
// Falls back to simple negation for backward compatibility.
|
||||||
const layoutToCanvasY = (value) => -Number(value || 0);
|
const canvasToLayoutY = (value) => { const v = Number(value || 0); return __exportOriginY != null ? __exportOriginY - v : -v; };
|
||||||
|
const canvasToLayoutX = (value) => { const v = Number(value || 0); return __exportOriginX != null ? v - __exportOriginX : v; };
|
||||||
|
// Convert layout/display coordinates back into canvas coordinates.
|
||||||
|
// Uses self-inverse formula for Y: rfY = originY - displayY (same as above).
|
||||||
|
// Import: layout/display coords → canvas coords. Optional originY/originX allow
|
||||||
|
// per-file overrides (e.g. from a YAML canvas_origin field). When omitted the
|
||||||
|
// module-level origin is used; null origin falls back to simple negation / pass-through.
|
||||||
|
const layoutToCanvasY = (value, originY) => { const v = Number(value || 0); const oy = originY !== undefined ? originY : __exportOriginY; return oy != null ? oy - v : -v; };
|
||||||
|
const layoutToCanvasX = (value, originX) => { const v = Number(value || 0); const ox = originX !== undefined ? originX : __exportOriginX; return ox != null ? v + ox : v; };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Serialize nested component settings into YAML blocks.
|
// Serialize nested component settings into YAML blocks.
|
||||||
const buildSettingsYaml = (settings, indent) => {
|
const buildSettingsYaml = (settings, indent) => {
|
||||||
@@ -677,8 +694,8 @@
|
|||||||
: '\n settings:\n length:';
|
: '\n settings:\n length:';
|
||||||
|
|
||||||
return ` ${instanceName}:
|
return ` ${instanceName}:
|
||||||
component: ${componentValue}
|
component: "${componentValue}"
|
||||||
x: ${Number(position.x || 0).toFixed(1)}
|
x: ${canvasToLayoutX(position.x).toFixed(1)}
|
||||||
y: ${canvasToLayoutY(position.y).toFixed(1)}
|
y: ${canvasToLayoutY(position.y).toFixed(1)}
|
||||||
rotation: ${Number(rotation || 0).toFixed(1)}
|
rotation: ${Number(rotation || 0).toFixed(1)}
|
||||||
flip: ${flip ? 1 : 0}
|
flip: ${flip ? 1 : 0}
|
||||||
@@ -908,7 +925,7 @@
|
|||||||
|
|
||||||
// Convert standalone port nodes into page-level layout pins.
|
// Convert standalone port nodes into page-level layout pins.
|
||||||
const buildPageComponentPins = (port, nodes) => {
|
const buildPageComponentPins = (port, nodes) => {
|
||||||
const portNodes = (nodes || []).filter(isPortElementNode);
|
const portNodes = (nodes || []).filter(n => n.id === 'page-port');
|
||||||
if (portNodes.length > 0) {
|
if (portNodes.length > 0) {
|
||||||
return portNodes.reduce((pins, node) => {
|
return portNodes.reduce((pins, node) => {
|
||||||
const data = node.data || {};
|
const data = node.data || {};
|
||||||
@@ -962,7 +979,7 @@
|
|||||||
${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'}
|
${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'}
|
||||||
element: ${info.element}
|
element: ${info.element}
|
||||||
pin: ${info.pin}
|
pin: ${info.pin}
|
||||||
x: ${Number(info.x || 0).toFixed(1)}
|
x: ${canvasToLayoutX(info.x).toFixed(1)}
|
||||||
y: ${canvasToLayoutY(info.y).toFixed(1)}
|
y: ${canvasToLayoutY(info.y).toFixed(1)}
|
||||||
angle: ${Number(info.a || 0).toFixed(1)}
|
angle: ${Number(info.a || 0).toFixed(1)}
|
||||||
width: ${Number(info.width || 0.5)}${description}`;
|
width: ${Number(info.width || 0.5)}${description}`;
|
||||||
@@ -977,7 +994,7 @@
|
|||||||
|
|
||||||
// Serialize built-in port and anchor nodes into layout element metadata.
|
// Serialize built-in port and anchor nodes into layout element metadata.
|
||||||
const buildElementsYaml = (nodes) => {
|
const buildElementsYaml = (nodes) => {
|
||||||
const elementNodes = (nodes || []).filter(isElementNode);
|
const elementNodes = (nodes || []).filter(n => isElementNode(n) && n.id !== 'page-port');
|
||||||
if (elementNodes.length === 0) return 'elements: {}';
|
if (elementNodes.length === 0) return 'elements: {}';
|
||||||
const lines = elementNodes.map(node => {
|
const lines = elementNodes.map(node => {
|
||||||
const data = node.data || {};
|
const data = node.data || {};
|
||||||
@@ -990,7 +1007,7 @@
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
return ` ${name}:
|
return ` ${name}:
|
||||||
type: ${data.elementType}
|
type: ${data.elementType}
|
||||||
x: ${Number((node.position && node.position.x) || 0).toFixed(1)}
|
x: ${canvasToLayoutX((node.position && node.position.x) || 0).toFixed(1)}
|
||||||
y: ${canvasToLayoutY((node.position && node.position.y) || 0).toFixed(1)}
|
y: ${canvasToLayoutY((node.position && node.position.y) || 0).toFixed(1)}
|
||||||
angle: ${Number(angle || 0).toFixed(1)}
|
angle: ${Number(angle || 0).toFixed(1)}
|
||||||
pin_number: ${portNumber}
|
pin_number: ${portNumber}
|
||||||
@@ -1055,7 +1072,7 @@ ${pinLines}`;
|
|||||||
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
|
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
|
||||||
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
|
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
|
||||||
const pointsYaml = points.length > 0
|
const pointsYaml = points.length > 0
|
||||||
? `\n points:\n${points.map(point => ` - x: ${Number(point.x || 0).toFixed(1)}\n y: ${canvasToLayoutY(point.y).toFixed(1)}`).join('\n')}`
|
? `\n points:\n${points.map(point => ` - x: ${canvasToLayoutX(point.x).toFixed(1)}\n y: ${canvasToLayoutY(point.y).toFixed(1)}`).join('\n')}`
|
||||||
: '';
|
: '';
|
||||||
const isFreeRoute = Boolean(edge.data && edge.data.freeRoute) || (!sourceNode && !targetNode && points.length >= 2);
|
const isFreeRoute = Boolean(edge.data && edge.data.freeRoute) || (!sourceNode && !targetNode && points.length >= 2);
|
||||||
const linkYaml = isFreeRoute
|
const linkYaml = isFreeRoute
|
||||||
@@ -1311,8 +1328,12 @@ bundles:${groupsYaml ? `\n${groupsYaml}` : ' {}'}`;
|
|||||||
DEFAULT_FORGE_ARGUMENTS,
|
DEFAULT_FORGE_ARGUMENTS,
|
||||||
FALLBACK_TECHNOLOGY_MANIFEST,
|
FALLBACK_TECHNOLOGY_MANIFEST,
|
||||||
FREE_WIRES_BUNDLE_GROUP,
|
FREE_WIRES_BUNDLE_GROUP,
|
||||||
|
setExportOrigin,
|
||||||
|
getExportOrigin,
|
||||||
canvasToLayoutY,
|
canvasToLayoutY,
|
||||||
|
canvasToLayoutX,
|
||||||
layoutToCanvasY,
|
layoutToCanvasY,
|
||||||
|
layoutToCanvasX,
|
||||||
createForgeArguments,
|
createForgeArguments,
|
||||||
createRouteSettings,
|
createRouteSettings,
|
||||||
updateRouteField,
|
updateRouteField,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<!-- CANVAS v22: per-page instance counter — reserveComponentDisplayNamesFromPages only scans active page, not whole project -->
|
||||||
<!--
|
<!--
|
||||||
Description: Main MXPIC EDA canvas UI with React Flow editing, project pages, routing, layout build controls, and inspector panels.
|
Description: Main MXPIC EDA canvas UI with React Flow editing, project pages, routing, layout build controls, and inspector panels.
|
||||||
Inside functions: fetchIcon, RotatableNode, PortNode, AnchorNode, RightPanel, findComponentPath, loadProject, handleBasicConnection, buildElementNodesFromYaml.
|
Inside functions: fetchIcon, RotatableNode, PortNode, AnchorNode, RightPanel, findComponentPath, loadProject, handleBasicConnection, buildElementNodesFromYaml.
|
||||||
@@ -1467,11 +1468,47 @@ Organization : OptiHK Limited
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 4px solid rgba(69, 214, 200, 0.92);
|
border: 4px solid rgba(69, 214, 200, 0.92);
|
||||||
background: rgba(69, 214, 200, 0.018);
|
background: rgba(69, 214, 200, 0.06);
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14), 0 0 22px rgba(69, 214, 200, 0.18);
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22), 0 0 28px rgba(69, 214, 200, 0.28);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.light-mode .canvas-boundary-node {
|
||||||
|
border: 4px solid rgba(8, 127, 115, 0.9);
|
||||||
|
background: rgba(8, 127, 115, 0.12);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7), 0 0 32px rgba(8, 127, 115, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.origin-marker-node {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.origin-marker-node::before,
|
||||||
|
.origin-marker-node::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
background: #ef4444;
|
||||||
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.origin-marker-node::before {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.origin-marker-node::after {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
.ruler-point-node {
|
.ruler-point-node {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@@ -1588,6 +1625,10 @@ Organization : OptiHK Limited
|
|||||||
createRulerMeasurement,
|
createRulerMeasurement,
|
||||||
createComponentSymbolMetrics,
|
createComponentSymbolMetrics,
|
||||||
FALLBACK_TECHNOLOGY_MANIFEST,
|
FALLBACK_TECHNOLOGY_MANIFEST,
|
||||||
|
setExportOrigin,
|
||||||
|
getExportOrigin,
|
||||||
|
canvasToLayoutX,
|
||||||
|
layoutToCanvasX,
|
||||||
layoutToCanvasY
|
layoutToCanvasY
|
||||||
} = window.MxpicCanvasHelpers;
|
} = window.MxpicCanvasHelpers;
|
||||||
|
|
||||||
@@ -1607,6 +1648,7 @@ Organization : OptiHK Limited
|
|||||||
// Loads and caches category icons so repeated library renders do not refetch the same image.
|
// Loads and caches category icons so repeated library renders do not refetch the same image.
|
||||||
function fetchIcon(category) {
|
function fetchIcon(category) {
|
||||||
if (!iconPromiseCache[category]) {
|
if (!iconPromiseCache[category]) {
|
||||||
|
|
||||||
let resolveFn;
|
let resolveFn;
|
||||||
const promise = new Promise((resolve) => {
|
const promise = new Promise((resolve) => {
|
||||||
resolveFn = resolve;
|
resolveFn = resolve;
|
||||||
@@ -2110,6 +2152,11 @@ Organization : OptiHK Limited
|
|||||||
<div className="canvas-boundary-node" title={`${data.size.width} x ${data.size.height} um`} />
|
<div className="canvas-boundary-node" title={`${data.size.width} x ${data.size.height} um`} />
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Red "X" marker at the canvas origin (0,0). Non-selectable, always visible.
|
||||||
|
const OriginMarkerNode = memo(({ data }) => (
|
||||||
|
<div className="origin-marker-node" title={`Canvas origin (${(data.originX ?? 0).toFixed(1)}, ${(data.originY ?? 0).toFixed(1)})`} />
|
||||||
|
));
|
||||||
|
|
||||||
// Draws invisible connection handles for ruler measurement endpoints.
|
// Draws invisible connection handles for ruler measurement endpoints.
|
||||||
const RulerPointNode = memo(({ data }) => {
|
const RulerPointNode = memo(({ data }) => {
|
||||||
const hiddenHandleStyle = {
|
const hiddenHandleStyle = {
|
||||||
@@ -2903,7 +2950,7 @@ Organization : OptiHK Limited
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Renders editable properties for selected nodes, ports, anchors, and routes.
|
// Renders editable properties for selected nodes, ports, anchors, and routes.
|
||||||
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], bundleGroupOptions = [], technologyManifest, projectName, compositeNames = [], width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
|
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], bundleGroupOptions = [], technologyManifest, projectName, compositeNames = [], width, canvasOrigin, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
|
||||||
const [componentData, setComponentData] = useState(null);
|
const [componentData, setComponentData] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [enlarged, setEnlarged] = useState(null);
|
const [enlarged, setEnlarged] = useState(null);
|
||||||
@@ -3001,8 +3048,8 @@ Organization : OptiHK Limited
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingTransformField) return;
|
if (editingTransformField) return;
|
||||||
if (selectedPositionNodes.length > 0) {
|
if (selectedPositionNodes.length > 0) {
|
||||||
setLocalX(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
|
setLocalX(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x - canvasOrigin.x));
|
||||||
setLocalY(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
|
setLocalY(getSharedNumericDisplay(selectedPositionNodes, node => canvasOrigin.y - node.position.y));
|
||||||
setLocalRotation(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
|
setLocalRotation(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3014,14 +3061,16 @@ Organization : OptiHK Limited
|
|||||||
const updatePosition = useCallback((id, axis, value) => {
|
const updatePosition = useCallback((id, axis, value) => {
|
||||||
const val = parseFloat(value);
|
const val = parseFloat(value);
|
||||||
if (isNaN(val)) return;
|
if (isNaN(val)) return;
|
||||||
|
// Convert display coordinates back to ReactFlow coordinates.
|
||||||
|
const rfVal = axis === 'y' ? canvasOrigin.y - val : val + canvasOrigin.x;
|
||||||
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) {
|
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) {
|
||||||
selectedPositionNodes.forEach(node => {
|
selectedPositionNodes.forEach(node => {
|
||||||
onUpdateNode(node.id, { position: { [axis]: val } });
|
onUpdateNode(node.id, { position: { [axis]: rfVal } });
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onUpdateNode(id, { position: { [axis]: val } });
|
onUpdateNode(id, { position: { [axis]: rfVal } });
|
||||||
}, [onUpdateNode, selectedPositionNodes]);
|
}, [onUpdateNode, selectedPositionNodes, canvasOrigin]);
|
||||||
|
|
||||||
const updateRotation = useCallback((id, value, isPortNode = false) => {
|
const updateRotation = useCallback((id, value, isPortNode = false) => {
|
||||||
const val = parseFloat(value);
|
const val = parseFloat(value);
|
||||||
@@ -3051,9 +3100,9 @@ Organization : OptiHK Limited
|
|||||||
}
|
}
|
||||||
setter(val.toFixed(3));
|
setter(val.toFixed(3));
|
||||||
} else if (field === 'x') {
|
} else if (field === 'x') {
|
||||||
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
|
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x - canvasOrigin.x));
|
||||||
} else if (field === 'y') {
|
} else if (field === 'y') {
|
||||||
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
|
setter(getSharedNumericDisplay(selectedPositionNodes, node => canvasOrigin.y - node.position.y));
|
||||||
} else {
|
} else {
|
||||||
setter(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
|
setter(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
|
||||||
}
|
}
|
||||||
@@ -3920,6 +3969,9 @@ Organization : OptiHK Limited
|
|||||||
const [gridSnap, setGridSnap] = useState(false);
|
const [gridSnap, setGridSnap] = useState(false);
|
||||||
const [canvasTextVisible, setCanvasTextVisible] = useState(true);
|
const [canvasTextVisible, setCanvasTextVisible] = useState(true);
|
||||||
const [themeMode, setThemeMode] = useState(() => localStorage.getItem('mxpic-theme') || 'dark');
|
const [themeMode, setThemeMode] = useState(() => localStorage.getItem('mxpic-theme') || 'dark');
|
||||||
|
const [closedTabs, setClosedTabs] = useState(() => {
|
||||||
|
try { return JSON.parse(localStorage.getItem('mxpic-closed-tabs') || '[]'); } catch { return []; }
|
||||||
|
});
|
||||||
const [logs, setLogs] = useState([{ time: new Date().toLocaleTimeString(), message: 'Editor ready.' }]);
|
const [logs, setLogs] = useState([{ time: new Date().toLocaleTimeString(), message: 'Editor ready.' }]);
|
||||||
const [buildLayoutBusy, setBuildLayoutBusy] = useState(false);
|
const [buildLayoutBusy, setBuildLayoutBusy] = useState(false);
|
||||||
const [buildGdsBusy, setBuildGdsBusy] = useState(false);
|
const [buildGdsBusy, setBuildGdsBusy] = useState(false);
|
||||||
@@ -3949,6 +4001,36 @@ Organization : OptiHK Limited
|
|||||||
const currentNodes = activePage && Array.isArray(activePage.nodes) ? activePage.nodes : [];
|
const currentNodes = activePage && Array.isArray(activePage.nodes) ? activePage.nodes : [];
|
||||||
const currentEdges = activePage && Array.isArray(activePage.edges) ? activePage.edges : [];
|
const currentEdges = activePage && Array.isArray(activePage.edges) ? activePage.edges : [];
|
||||||
const activeCanvasSize = useMemo(() => normalizeCanvasSize(activePage?.canvasSize), [activePage?.canvasSize]);
|
const activeCanvasSize = useMemo(() => normalizeCanvasSize(activePage?.canvasSize), [activePage?.canvasSize]);
|
||||||
|
// Track viewport dimensions to compute dynamic zoom limits.
|
||||||
|
const [canvasViewportSize, setCanvasViewportSize] = React.useState({ width: 1200, height: 800 });
|
||||||
|
// Set origin from the active page's saved value, or default to (500, H-500).
|
||||||
|
useEffect(() => {
|
||||||
|
if (activePage?.origin) {
|
||||||
|
setCanvasOrigin({ x: activePage.origin.x, y: activePage.origin.y });
|
||||||
|
} else if (activePage) {
|
||||||
|
setCanvasOrigin({ x: 500, y: activeCanvasSize.height - 500 });
|
||||||
|
}
|
||||||
|
}, [activePage?.id, activePage?.origin?.x, activePage?.origin?.y, activeCanvasSize.height]);
|
||||||
|
|
||||||
|
// Observe viewport size changes to recalculate minZoom.
|
||||||
|
useEffect(() => {
|
||||||
|
const el = canvasViewportRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const update = () => setCanvasViewportSize({ width: el.clientWidth, height: el.clientHeight });
|
||||||
|
update();
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
const ro = new ResizeObserver(update);
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', update);
|
||||||
|
return () => window.removeEventListener('resize', update);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Keep the global export origin in sync so YAML coordinates match RightPanel display.
|
||||||
|
useEffect(() => {
|
||||||
|
setExportOrigin(canvasOrigin.x, canvasOrigin.y);
|
||||||
|
}, [canvasOrigin.x, canvasOrigin.y]);
|
||||||
const selectedEdges = useMemo(() => currentEdges.filter(edge => edge.selected), [currentEdges]);
|
const selectedEdges = useMemo(() => currentEdges.filter(edge => edge.selected), [currentEdges]);
|
||||||
const selectedEdge = selectedEdges[0] || null;
|
const selectedEdge = selectedEdges[0] || null;
|
||||||
const selectedNodes = useMemo(() => currentNodes.filter(n => n.selected), [currentNodes]);
|
const selectedNodes = useMemo(() => currentNodes.filter(n => n.selected), [currentNodes]);
|
||||||
@@ -3992,6 +4074,15 @@ Organization : OptiHK Limited
|
|||||||
}
|
}
|
||||||
}, [linkXsectionChoices, currentLinkXsection]);
|
}, [linkXsectionChoices, currentLinkXsection]);
|
||||||
const canvasNodeExtent = useMemo(() => [[0, 0], [activeCanvasSize.width, activeCanvasSize.height]], [activeCanvasSize.width, activeCanvasSize.height]);
|
const canvasNodeExtent = useMemo(() => [[0, 0], [activeCanvasSize.width, activeCanvasSize.height]], [activeCanvasSize.width, activeCanvasSize.height]);
|
||||||
|
// Dynamic minZoom: viewport can show at most 120% of canvas size.
|
||||||
|
const canvasMinZoom = useMemo(() => {
|
||||||
|
const cw = activeCanvasSize.width;
|
||||||
|
const ch = activeCanvasSize.height;
|
||||||
|
const vw = canvasViewportSize.width;
|
||||||
|
const vh = canvasViewportSize.height;
|
||||||
|
if (!cw || !ch || !vw || !vh) return 0.05;
|
||||||
|
return Math.min(vw / cw, vh / ch) / 1.2;
|
||||||
|
}, [activeCanvasSize.width, activeCanvasSize.height, canvasViewportSize.width, canvasViewportSize.height]);
|
||||||
const rulerActiveEndPoint = rulerEndPoint || rulerPreviewPoint;
|
const rulerActiveEndPoint = rulerEndPoint || rulerPreviewPoint;
|
||||||
const rulerMeasurement = useMemo(
|
const rulerMeasurement = useMemo(
|
||||||
() => createRulerMeasurement(rulerStartPoint, rulerActiveEndPoint),
|
() => createRulerMeasurement(rulerStartPoint, rulerActiveEndPoint),
|
||||||
@@ -4002,7 +4093,7 @@ Organization : OptiHK Limited
|
|||||||
mouseCanvasPoint
|
mouseCanvasPoint
|
||||||
? {
|
? {
|
||||||
x: Number((mouseCanvasPoint.x - canvasOrigin.x).toFixed(3)),
|
x: Number((mouseCanvasPoint.x - canvasOrigin.x).toFixed(3)),
|
||||||
y: Number((mouseCanvasPoint.y - canvasOrigin.y).toFixed(3))
|
y: Number((canvasOrigin.y - mouseCanvasPoint.y).toFixed(3))
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
), [mouseCanvasPoint, canvasOrigin]);
|
), [mouseCanvasPoint, canvasOrigin]);
|
||||||
@@ -4149,8 +4240,18 @@ Organization : OptiHK Limited
|
|||||||
deletable: false,
|
deletable: false,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
style: { width: activeCanvasSize.width, height: activeCanvasSize.height, zIndex: -1, pointerEvents: 'none' }
|
style: { width: activeCanvasSize.width, height: activeCanvasSize.height, zIndex: -1, pointerEvents: 'none' }
|
||||||
|
}, {
|
||||||
|
id: '__origin-marker__',
|
||||||
|
type: 'originMarkerNode',
|
||||||
|
position: { x: canvasOrigin.x, y: canvasOrigin.y },
|
||||||
|
data: { originX: canvasOrigin.x, originY: canvasOrigin.y },
|
||||||
|
draggable: false,
|
||||||
|
selectable: false,
|
||||||
|
deletable: false,
|
||||||
|
focusable: false,
|
||||||
|
style: { zIndex: 10, pointerEvents: 'none' }
|
||||||
}, ...currentNodes, ...freeRouteEndpointNodes, ...rulerNodes];
|
}, ...currentNodes, ...freeRouteEndpointNodes, ...rulerNodes];
|
||||||
}, [activePage, currentNodes, activeCanvasSize, freeRouteEndpointNodes, rulerNodes]);
|
}, [activePage, currentNodes, activeCanvasSize, canvasOrigin, freeRouteEndpointNodes, rulerNodes]);
|
||||||
// Resolves rotated anchor handle direction so connected canvas links exit the correct side.
|
// Resolves rotated anchor handle direction so connected canvas links exit the correct side.
|
||||||
const getAnchorHandleRouteDirection = useCallback((node, handleId) => {
|
const getAnchorHandleRouteDirection = useCallback((node, handleId) => {
|
||||||
if (!node || !handleId || !(node.type === 'anchorNode' || node.data?.elementType === 'anchor')) return null;
|
if (!node || !handleId || !(node.type === 'anchorNode' || node.data?.elementType === 'anchor')) return null;
|
||||||
@@ -4253,12 +4354,12 @@ Organization : OptiHK Limited
|
|||||||
value === true || value === 1 || value === '1' || String(value).toLowerCase() === 'true'
|
value === true || value === 1 || value === '1' || String(value).toLowerCase() === 'true'
|
||||||
), []);
|
), []);
|
||||||
|
|
||||||
// Normalize stored route points and convert layout Y coordinates when needed.
|
// Normalize stored route points and convert layout coordinates back to canvas coordinates.
|
||||||
const normalizeRoutePoints = useCallback((points, usesGdsYUp = false) => (
|
const normalizeRoutePoints = useCallback((points, usesGdsYUp = false) => (
|
||||||
(Array.isArray(points) ? points : [])
|
(Array.isArray(points) ? points : [])
|
||||||
.map(point => ({
|
.map(point => ({
|
||||||
x: Number(point && point.x),
|
x: usesGdsYUp ? layoutToCanvasX(point && point.x) : Number(point && point.x || 0),
|
||||||
y: usesGdsYUp ? layoutToCanvasY(point && point.y) : Number(point && point.y)
|
y: usesGdsYUp ? layoutToCanvasY(point && point.y) : Number(point && point.y || 0)
|
||||||
}))
|
}))
|
||||||
.filter(point => Number.isFinite(point.x) && Number.isFinite(point.y))
|
.filter(point => Number.isFinite(point.x) && Number.isFinite(point.y))
|
||||||
), []);
|
), []);
|
||||||
@@ -4297,7 +4398,10 @@ Organization : OptiHK Limited
|
|||||||
return componentDataCacheRef.current.get(componentName);
|
return componentDataCacheRef.current.get(componentName);
|
||||||
}
|
}
|
||||||
const response = await fetch(`/api/component/${encodeURIComponent(componentName)}?project=${encodeURIComponent(currentProjectName)}`);
|
const response = await fetch(`/api/component/${encodeURIComponent(componentName)}?project=${encodeURIComponent(currentProjectName)}`);
|
||||||
if (!response.ok) return null;
|
if (!response.ok) {
|
||||||
|
componentDataCacheRef.current.set(componentName, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
componentDataCacheRef.current.set(componentName, data);
|
componentDataCacheRef.current.set(componentName, data);
|
||||||
return data;
|
return data;
|
||||||
@@ -4359,7 +4463,7 @@ Organization : OptiHK Limited
|
|||||||
// Apply React Flow node changes while preserving canvas-only helper nodes.
|
// Apply React Flow node changes while preserving canvas-only helper nodes.
|
||||||
const onNodesChange = useCallback((changes) => {
|
const onNodesChange = useCallback((changes) => {
|
||||||
if (!activePageId) return;
|
if (!activePageId) return;
|
||||||
const relevantChanges = changes.filter(change => change.id !== '__canvas-boundary__');
|
const relevantChanges = changes.filter(change => change.id !== '__canvas-boundary__' && change.id !== '__origin-marker__');
|
||||||
if (relevantChanges.length === 0) return;
|
if (relevantChanges.length === 0) return;
|
||||||
const removedNodeIds = new Set(relevantChanges.filter(change => change.type === 'remove').map(change => change.id));
|
const removedNodeIds = new Set(relevantChanges.filter(change => change.type === 'remove').map(change => change.id));
|
||||||
if (removedNodeIds.size > 0 && activePage) {
|
if (removedNodeIds.size > 0 && activePage) {
|
||||||
@@ -4714,12 +4818,18 @@ Organization : OptiHK Limited
|
|||||||
nodes.forEach(node => releaseComponentDisplayName(node?.data?.componentDisplayName));
|
nodes.forEach(node => releaseComponentDisplayName(node?.data?.componentDisplayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild the used-name index table from all currently loaded pages.
|
// Rebuild the used-name index table from the current active page only.
|
||||||
function reserveComponentDisplayNamesFromPages() {
|
function reserveComponentDisplayNamesFromPages() {
|
||||||
pages.forEach(page => {
|
componentIndexesByPrefixRef.current = {};
|
||||||
(page.nodes || []).forEach(node => reserveComponentDisplayName(node?.data?.componentDisplayName));
|
if (activePage) {
|
||||||
|
(activePage.nodes || []).forEach(node => {
|
||||||
|
const name = node?.data?.componentDisplayName;
|
||||||
|
if (name) {
|
||||||
|
reserveComponentDisplayName(name);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Convert a component category into the saved display-name prefix or abbreviation.
|
// Convert a component category into the saved display-name prefix or abbreviation.
|
||||||
const normalizeComponentDisplayNamePrefix = useCallback((prefixSource, options = {}) => {
|
const normalizeComponentDisplayNamePrefix = useCallback((prefixSource, options = {}) => {
|
||||||
@@ -4858,7 +4968,7 @@ Organization : OptiHK Limited
|
|||||||
id: nodeId,
|
id: nodeId,
|
||||||
type: 'portNode',
|
type: 'portNode',
|
||||||
position: {
|
position: {
|
||||||
x,
|
x: usesGdsYUp ? layoutToCanvasX(x) : x,
|
||||||
y: usesGdsYUp ? layoutToCanvasY(y) : y,
|
y: usesGdsYUp ? layoutToCanvasY(y) : y,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
@@ -4875,7 +4985,7 @@ Organization : OptiHK Limited
|
|||||||
id: nodeId,
|
id: nodeId,
|
||||||
type: 'anchorNode',
|
type: 'anchorNode',
|
||||||
position: {
|
position: {
|
||||||
x,
|
x: usesGdsYUp ? layoutToCanvasX(x) : x,
|
||||||
y: usesGdsYUp ? layoutToCanvasY(y) : y,
|
y: usesGdsYUp ? layoutToCanvasY(y) : y,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
@@ -4910,6 +5020,9 @@ Organization : OptiHK Limited
|
|||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const doc = jsyaml.load(text);
|
const doc = jsyaml.load(text);
|
||||||
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
||||||
|
const savedOrigin = getExportOrigin();
|
||||||
|
const docOrigin = doc.origin ? { x: Number(doc.origin.x || 0), y: Number(doc.origin.y || 0) } : null;
|
||||||
|
if (docOrigin) setExportOrigin(docOrigin.x, docOrigin.y);
|
||||||
if (!doc.instances && !doc.elements) {
|
if (!doc.instances && !doc.elements) {
|
||||||
alert('no instances or elements found');
|
alert('no instances or elements found');
|
||||||
return;
|
return;
|
||||||
@@ -4927,13 +5040,14 @@ Organization : OptiHK Limited
|
|||||||
const compPath = inst.component || '';
|
const compPath = inst.component || '';
|
||||||
const compName = compPath.split('/').pop();
|
const compName = compPath.split('/').pop();
|
||||||
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
|
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
|
||||||
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
|
const instIsPDK = compPath.includes('/');
|
||||||
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
|
const instIsBasic = !instIsPDK && (isBasicComponent(compPath) || isBasicComponent(compName));
|
||||||
|
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : compName;
|
||||||
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
|
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
|
||||||
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
|
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
|
||||||
let category = '';
|
let category = '';
|
||||||
|
|
||||||
if (!isProject && displayCompName && library && !instIsForge) {
|
if (displayCompName && library && !instIsForge) {
|
||||||
const walk = (obj) => {
|
const walk = (obj) => {
|
||||||
if (obj?.__type__ === 'component' && obj.__name__ === displayCompName) {
|
if (obj?.__type__ === 'component' && obj.__name__ === displayCompName) {
|
||||||
category = obj.__category__ || '';
|
category = obj.__category__ || '';
|
||||||
@@ -4945,6 +5059,9 @@ Organization : OptiHK Limited
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
walk(library);
|
walk(library);
|
||||||
|
if (!category && compPath.includes('/')) {
|
||||||
|
category = compPath.split('/').slice(-2, -1)[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||||
@@ -4954,12 +5071,12 @@ Organization : OptiHK Limited
|
|||||||
id: nodeId,
|
id: nodeId,
|
||||||
type: 'rotatableNode',
|
type: 'rotatableNode',
|
||||||
position: {
|
position: {
|
||||||
x: parseFloat(inst.x) || 0,
|
x: usesGdsYUp ? layoutToCanvasX(inst.x) : (parseFloat(inst.x) || 0),
|
||||||
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
|
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
label: isProject ? instName : displayCompName,
|
label: isProject ? instName : displayCompName,
|
||||||
componentName: isProject ? instName : displayCompName,
|
componentName: displayCompName,
|
||||||
category: isProject ? '' : category,
|
category: isProject ? '' : category,
|
||||||
rotation: parseFloat(inst.rotation) || 0,
|
rotation: parseFloat(inst.rotation) || 0,
|
||||||
flip: toBooleanFlag(inst.flip ?? inst.mirror),
|
flip: toBooleanFlag(inst.flip ?? inst.mirror),
|
||||||
@@ -4976,6 +5093,30 @@ Organization : OptiHK Limited
|
|||||||
}
|
}
|
||||||
newNodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
|
newNodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
|
||||||
|
|
||||||
|
// Pre-fetch PDK component metadata so imported nodes render with correct boxSize/ports/icons.
|
||||||
|
const pdkNames = [...new Set(newNodes
|
||||||
|
.filter(n => n.data?.componentName && !n.data?.elementType
|
||||||
|
&& !isForgeComponent(n.data.componentName)
|
||||||
|
&& !isBasicComponent(n.data.componentName))
|
||||||
|
.map(n => n.data.componentName))];
|
||||||
|
if (pdkNames.length > 0) {
|
||||||
|
const metaResults = await Promise.all(
|
||||||
|
pdkNames.map(name => loadComponentMetadata(name).catch(() => null))
|
||||||
|
);
|
||||||
|
const metaMap = new Map(
|
||||||
|
pdkNames.filter((_, i) => metaResults[i]).map((name, i) => [name, metaResults[i]])
|
||||||
|
);
|
||||||
|
newNodes.forEach((node, idx) => {
|
||||||
|
const metadata = metaMap.get(node.data?.componentName);
|
||||||
|
if (!metadata) return;
|
||||||
|
const sz = normalizeBoxSize(metadata);
|
||||||
|
newNodes[idx] = {
|
||||||
|
...node,
|
||||||
|
data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '', category: node.data.category || metadata.__category__ || '' }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
forEachBundleLink(doc, (bundleName, bundle, link) => {
|
forEachBundleLink(doc, (bundleName, bundle, link) => {
|
||||||
const route = createRouteSettings(technologyManifest, { ...bundle, ...link, bundle_group: bundleName });
|
const route = createRouteSettings(technologyManifest, { ...bundle, ...link, bundle_group: bundleName });
|
||||||
const routePoints = normalizeRoutePoints(link.points, doc.coordinate_system === 'gds_y_up');
|
const routePoints = normalizeRoutePoints(link.points, doc.coordinate_system === 'gds_y_up');
|
||||||
@@ -5010,8 +5151,8 @@ Organization : OptiHK Limited
|
|||||||
const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5);
|
const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5);
|
||||||
const newPageName = file.name.replace(/\.(yaml|yml)$/i, '');
|
const newPageName = file.name.replace(/\.(yaml|yml)$/i, '');
|
||||||
const importedPin = Array.isArray(doc.pins) && doc.pins[0]
|
const importedPin = Array.isArray(doc.pins) && doc.pins[0]
|
||||||
? { x: Number(doc.pins[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.pins[0].y) : Number(doc.pins[0].y || 0), a: Number(doc.pins[0].angle ?? doc.pins[0].a ?? 0), width: Number(doc.pins[0].width || 0.5) }
|
? { x: usesGdsYUp ? layoutToCanvasX(doc.pins[0].x) : Number(doc.pins[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.pins[0].y) : Number(doc.pins[0].y || 0), a: Number(doc.pins[0].angle ?? doc.pins[0].a ?? 0), width: Number(doc.pins[0].width || 0.5) }
|
||||||
: { x: 50, y: 150, a: 0, width: 0.5 };
|
: { x: 600, y: 4400, a: 0, width: 0.5 };
|
||||||
const newPage = {
|
const newPage = {
|
||||||
id: newPageId,
|
id: newPageId,
|
||||||
name: newPageName,
|
name: newPageName,
|
||||||
@@ -5031,8 +5172,12 @@ Organization : OptiHK Limited
|
|||||||
],
|
],
|
||||||
edges: newEdges,
|
edges: newEdges,
|
||||||
port: importedPin,
|
port: importedPin,
|
||||||
|
origin: docOrigin || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Restore the previous origin now that all coordinate conversions are done.
|
||||||
|
if (docOrigin) setExportOrigin(savedOrigin.x, savedOrigin.y);
|
||||||
|
|
||||||
setPages(prev => [...prev, newPage]);
|
setPages(prev => [...prev, newPage]);
|
||||||
setActivePageId(newPageId);
|
setActivePageId(newPageId);
|
||||||
|
|
||||||
@@ -5081,7 +5226,7 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
input.addEventListener('change', handleFile);
|
input.addEventListener('change', handleFile);
|
||||||
return () => input.removeEventListener('change', handleFile);
|
return () => input.removeEventListener('change', handleFile);
|
||||||
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]);
|
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle, loadComponentMetadata]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProjectCompositeMap(prev => {
|
setProjectCompositeMap(prev => {
|
||||||
@@ -5106,10 +5251,20 @@ Organization : OptiHK Limited
|
|||||||
id: `project-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
id: `project-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
||||||
name: currentProjectName,
|
name: currentProjectName,
|
||||||
type: 'project',
|
type: 'project',
|
||||||
nodes: [],
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'page-port',
|
||||||
|
type: 'portNode',
|
||||||
|
position: { x: 600, y: 4400 },
|
||||||
|
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
|
||||||
|
draggable: true,
|
||||||
|
selectable: true,
|
||||||
|
deletable: false,
|
||||||
|
}
|
||||||
|
],
|
||||||
edges: [],
|
edges: [],
|
||||||
canvasSize: DEFAULT_CANVAS_SIZE,
|
canvasSize: DEFAULT_CANVAS_SIZE,
|
||||||
port: { x: 0, y: 0, a: 0 }
|
port: { x: 600, y: 4400, a: 0 }
|
||||||
});
|
});
|
||||||
|
|
||||||
const findCategory = (compName) => {
|
const findCategory = (compName) => {
|
||||||
@@ -5131,13 +5286,18 @@ Organization : OptiHK Limited
|
|||||||
const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
|
const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
|
||||||
const doc = jsyaml.load(content) || {};
|
const doc = jsyaml.load(content) || {};
|
||||||
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
||||||
|
// Temporarily use the YAML's origin for coordinate conversion (if present)
|
||||||
|
// so that instances/ports/routes land at the correct canvas positions.
|
||||||
|
const savedOrigin = getExportOrigin();
|
||||||
|
const docOrigin = doc.origin ? { x: Number(doc.origin.x || 0), y: Number(doc.origin.y || 0) } : null;
|
||||||
|
if (docOrigin) setExportOrigin(docOrigin.x, docOrigin.y);
|
||||||
const firstPin = Array.isArray(doc.pins) ? doc.pins[0] : null;
|
const firstPin = Array.isArray(doc.pins) ? doc.pins[0] : null;
|
||||||
const pagePort = firstPin
|
const pagePort = firstPin
|
||||||
? { x: Number(firstPin.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPin.y) : Number(firstPin.y || 0), a: Number(firstPin.angle ?? firstPin.a ?? 0), width: Number(firstPin.width || 0.5) }
|
? { x: usesGdsYUp ? layoutToCanvasX(firstPin.x) : Number(firstPin.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPin.y) : Number(firstPin.y || 0), a: Number(firstPin.angle ?? firstPin.a ?? 0), width: Number(firstPin.width || 0.5) }
|
||||||
: { x: 50, y: 150, a: 0, width: 0.5 };
|
: null;
|
||||||
const nodeNameMap = {};
|
const nodes = [];
|
||||||
const nodes = [
|
if (pagePort) {
|
||||||
{
|
nodes.push({
|
||||||
id: 'page-port',
|
id: 'page-port',
|
||||||
type: 'portNode',
|
type: 'portNode',
|
||||||
position: { x: pagePort.x, y: pagePort.y },
|
position: { x: pagePort.x, y: pagePort.y },
|
||||||
@@ -5145,17 +5305,21 @@ Organization : OptiHK Limited
|
|||||||
draggable: true,
|
draggable: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
deletable: false,
|
deletable: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
];
|
const nodeNameMap = {};
|
||||||
const edges = [];
|
const edges = [];
|
||||||
|
if (pagePort) {
|
||||||
nodeNameMap.port = 'page-port';
|
nodeNameMap.port = 'page-port';
|
||||||
|
}
|
||||||
|
|
||||||
Object.entries(doc.instances || {}).forEach(([instName, inst]) => {
|
Object.entries(doc.instances || {}).forEach(([instName, inst]) => {
|
||||||
const compPath = inst.component || '';
|
const compPath = inst.component || '';
|
||||||
const compName = compPath.split('/').pop();
|
const compName = compPath.split('/').pop();
|
||||||
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
|
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
|
||||||
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
|
const instIsPDK = compPath.includes('/');
|
||||||
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
|
const instIsBasic = !instIsPDK && (isBasicComponent(compPath) || isBasicComponent(compName));
|
||||||
|
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : compName;
|
||||||
const instIsComposite = knownCompositeNames.has(compName);
|
const instIsComposite = knownCompositeNames.has(compName);
|
||||||
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
|
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
|
||||||
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
|
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
|
||||||
@@ -5165,13 +5329,13 @@ Organization : OptiHK Limited
|
|||||||
id: nodeId,
|
id: nodeId,
|
||||||
type: 'rotatableNode',
|
type: 'rotatableNode',
|
||||||
position: {
|
position: {
|
||||||
x: parseFloat(inst.x) || 0,
|
x: usesGdsYUp ? layoutToCanvasX(inst.x) : (parseFloat(inst.x) || 0),
|
||||||
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
|
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
label: instIsComposite ? instName : displayCompName,
|
label: instIsComposite ? instName : displayCompName,
|
||||||
componentName: instIsComposite ? compName : displayCompName,
|
componentName: instIsComposite ? compName : displayCompName,
|
||||||
category: instIsComposite || instIsForge ? '' : findCategory(displayCompName),
|
category: instIsComposite || instIsForge ? '' : (findCategory(displayCompName) || (compPath.includes('/') ? compPath.split('/').slice(-2, -1)[0] : '')),
|
||||||
rotation: parseFloat(inst.rotation) || 0,
|
rotation: parseFloat(inst.rotation) || 0,
|
||||||
flip: toBooleanFlag(inst.flip ?? inst.mirror),
|
flip: toBooleanFlag(inst.flip ?? inst.mirror),
|
||||||
flop: toBooleanFlag(inst.flop),
|
flop: toBooleanFlag(inst.flop),
|
||||||
@@ -5185,7 +5349,15 @@ Organization : OptiHK Limited
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
nodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
|
// Strip port entries from elements that duplicate page-port already created from pins above.
|
||||||
|
const elementsWithoutPorts = doc.elements
|
||||||
|
? Object.fromEntries(Object.entries(doc.elements).filter(([elName, el]) => !(el && el.type === 'port' && nodeNameMap[elName])))
|
||||||
|
: undefined;
|
||||||
|
nodes.push(...buildElementNodesFromYaml(
|
||||||
|
elementsWithoutPorts ? { ...doc, elements: elementsWithoutPorts } : doc,
|
||||||
|
usesGdsYUp,
|
||||||
|
nodeNameMap
|
||||||
|
));
|
||||||
|
|
||||||
forEachBundleLink(doc, (bundleName, bundle, link) => {
|
forEachBundleLink(doc, (bundleName, bundle, link) => {
|
||||||
const route = createRouteSettings(manifest, { ...bundle, ...link, bundle_group: bundleName });
|
const route = createRouteSettings(manifest, { ...bundle, ...link, bundle_group: bundleName });
|
||||||
@@ -5217,6 +5389,9 @@ Organization : OptiHK Limited
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restore the previous origin after parsing.
|
||||||
|
if (docOrigin) setExportOrigin(savedOrigin.x, savedOrigin.y);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `cell-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
id: `cell-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
||||||
name: doc.name || cellName,
|
name: doc.name || cellName,
|
||||||
@@ -5224,7 +5399,8 @@ Organization : OptiHK Limited
|
|||||||
canvasSize: normalizeCanvasSize(doc.canvas_size || doc.canvasSize),
|
canvasSize: normalizeCanvasSize(doc.canvas_size || doc.canvasSize),
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
port: pagePort
|
port: pagePort || { x: 0, y: 0, a: 0 },
|
||||||
|
origin: docOrigin || undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -5272,7 +5448,8 @@ Organization : OptiHK Limited
|
|||||||
const pdkNames = [...new Set(allNodes
|
const pdkNames = [...new Set(allNodes
|
||||||
.filter(n => n.data?.componentName && !n.data?.elementType
|
.filter(n => n.data?.componentName && !n.data?.elementType
|
||||||
&& !isForgeComponent(n.data.componentName)
|
&& !isForgeComponent(n.data.componentName)
|
||||||
&& !isBasicComponent(n.data.componentName))
|
&& !(n.data?.type === 'composite')
|
||||||
|
&& !(n.data?.boxSize && n.data?.ports))
|
||||||
.map(n => n.data.componentName))];
|
.map(n => n.data.componentName))];
|
||||||
if (pdkNames.length > 0) {
|
if (pdkNames.length > 0) {
|
||||||
const metaResults = await Promise.all(
|
const metaResults = await Promise.all(
|
||||||
@@ -5289,7 +5466,7 @@ Organization : OptiHK Limited
|
|||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, sz),
|
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, sz),
|
||||||
data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '' }
|
data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '', category: node.data.category || metadata.__category__ || '' }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -5297,8 +5474,14 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
|
const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
|
||||||
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
|
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
|
||||||
|
// Respect closed-tab state so tabs closed before refresh stay closed.
|
||||||
|
let closedTabNames;
|
||||||
|
try { closedTabNames = JSON.parse(localStorage.getItem('mxpic-closed-tabs') || '[]'); } catch { closedTabNames = []; }
|
||||||
const resolvedProjectPage = loadedProjectPage || projectPage;
|
const resolvedProjectPage = loadedProjectPage || projectPage;
|
||||||
setPages([resolvedProjectPage, ...nonProjectPages]);
|
setPages([
|
||||||
|
resolvedProjectPage,
|
||||||
|
...nonProjectPages.map(p => closedTabNames.includes(p.name) ? { ...p, isClosed: true } : p)
|
||||||
|
]);
|
||||||
setActivePageId(resolvedProjectPage.id);
|
setActivePageId(resolvedProjectPage.id);
|
||||||
setProjectCompositeMap({ [currentProjectName]: nonProjectPages.map(page => page.name) });
|
setProjectCompositeMap({ [currentProjectName]: nonProjectPages.map(page => page.name) });
|
||||||
setStandaloneComposites([]);
|
setStandaloneComposites([]);
|
||||||
@@ -5374,6 +5557,7 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const missingPortNodes = [];
|
const missingPortNodes = [];
|
||||||
|
|
||||||
pages.forEach(page => {
|
pages.forEach(page => {
|
||||||
page.nodes.forEach(node => {
|
page.nodes.forEach(node => {
|
||||||
const componentName = node.data?.componentName;
|
const componentName = node.data?.componentName;
|
||||||
@@ -5461,6 +5645,13 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
// Open an existing project page by name.
|
// Open an existing project page by name.
|
||||||
const openProject = useCallback((name) => {
|
const openProject = useCallback((name) => {
|
||||||
|
// Re-opening a previously closed tab — remove from closed list.
|
||||||
|
setClosedTabs(current => {
|
||||||
|
if (!current.includes(name)) return current;
|
||||||
|
const next = current.filter(n => n !== name);
|
||||||
|
localStorage.setItem('mxpic-closed-tabs', JSON.stringify(next));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
setPages(prev => {
|
setPages(prev => {
|
||||||
const existing = prev.find(p => p.name === name && p.type === 'project');
|
const existing = prev.find(p => p.name === name && p.type === 'project');
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -5471,10 +5662,20 @@ Organization : OptiHK Limited
|
|||||||
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
|
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
|
||||||
name: name,
|
name: name,
|
||||||
type: 'project',
|
type: 'project',
|
||||||
nodes: [],
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'page-port',
|
||||||
|
type: 'portNode',
|
||||||
|
position: { x: 600, y: 4400 },
|
||||||
|
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
|
||||||
|
draggable: true,
|
||||||
|
selectable: true,
|
||||||
|
deletable: false,
|
||||||
|
}
|
||||||
|
],
|
||||||
edges: [],
|
edges: [],
|
||||||
canvasSize: DEFAULT_CANVAS_SIZE,
|
canvasSize: DEFAULT_CANVAS_SIZE,
|
||||||
port: { x: 0, y: 0, a: 0 }
|
port: { x: 600, y: 4400, a: 0 }
|
||||||
};
|
};
|
||||||
setActivePageId(newProjectPage.id);
|
setActivePageId(newProjectPage.id);
|
||||||
setProjectCompositeMap(prevMap => ({ ...prevMap, [name]: prevMap[name] || [] }));
|
setProjectCompositeMap(prevMap => ({ ...prevMap, [name]: prevMap[name] || [] }));
|
||||||
@@ -5484,6 +5685,12 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
// Open a canvas tab and make it active.
|
// Open a canvas tab and make it active.
|
||||||
const openPage = useCallback((name) => {
|
const openPage = useCallback((name) => {
|
||||||
|
setClosedTabs(current => {
|
||||||
|
if (!current.includes(name)) return current;
|
||||||
|
const next = current.filter(n => n !== name);
|
||||||
|
localStorage.setItem('mxpic-closed-tabs', JSON.stringify(next));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
const belongsToProject = Object.values(projectCompositeMap).some(comps => comps.includes(name));
|
const belongsToProject = Object.values(projectCompositeMap).some(comps => comps.includes(name));
|
||||||
if (!belongsToProject && !standaloneComposites.includes(name)) {
|
if (!belongsToProject && !standaloneComposites.includes(name)) {
|
||||||
setStandaloneComposites(prev => [...prev, name]);
|
setStandaloneComposites(prev => [...prev, name]);
|
||||||
@@ -5504,7 +5711,7 @@ Organization : OptiHK Limited
|
|||||||
{
|
{
|
||||||
id: 'page-port',
|
id: 'page-port',
|
||||||
type: 'portNode',
|
type: 'portNode',
|
||||||
position: { x: 50, y: 150 },
|
position: { x: 600, y: 4400 },
|
||||||
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
|
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
|
||||||
draggable: true,
|
draggable: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
@@ -5512,7 +5719,7 @@ Organization : OptiHK Limited
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
edges: [],
|
edges: [],
|
||||||
port: { x: 50, y: 150, a: 0 }
|
port: { x: 600, y: 4400, a: 0 }
|
||||||
};
|
};
|
||||||
setActivePageId(newComposite.id);
|
setActivePageId(newComposite.id);
|
||||||
return [...prev, newComposite];
|
return [...prev, newComposite];
|
||||||
@@ -5596,7 +5803,7 @@ Organization : OptiHK Limited
|
|||||||
{
|
{
|
||||||
id: 'page-port',
|
id: 'page-port',
|
||||||
type: 'portNode',
|
type: 'portNode',
|
||||||
position: { x: 50, y: 150 },
|
position: { x: 600, y: 4400 },
|
||||||
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
|
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
|
||||||
draggable: true,
|
draggable: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
@@ -5604,7 +5811,7 @@ Organization : OptiHK Limited
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
edges: [],
|
edges: [],
|
||||||
port: { x: 50, y: 150, a: 0 }
|
port: { x: 600, y: 4400, a: 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
setPages(prev => [...prev, newCell]);
|
setPages(prev => [...prev, newCell]);
|
||||||
@@ -5619,6 +5826,15 @@ Organization : OptiHK Limited
|
|||||||
// Close a canvas tab without deleting its saved content.
|
// Close a canvas tab without deleting its saved content.
|
||||||
const closePage = useCallback((pageId) => {
|
const closePage = useCallback((pageId) => {
|
||||||
setPages(prev => {
|
setPages(prev => {
|
||||||
|
const target = prev.find(p => p.id === pageId);
|
||||||
|
if (target) {
|
||||||
|
setClosedTabs(current => {
|
||||||
|
if (current.includes(target.name)) return current;
|
||||||
|
const next = [...current, target.name];
|
||||||
|
localStorage.setItem('mxpic-closed-tabs', JSON.stringify(next));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
const closed = prev.map(p => p.id === pageId ? { ...p, isClosed: true } : p);
|
const closed = prev.map(p => p.id === pageId ? { ...p, isClosed: true } : p);
|
||||||
if (activePageId === pageId) {
|
if (activePageId === pageId) {
|
||||||
const idx = prev.findIndex(p => p.id === pageId);
|
const idx = prev.findIndex(p => p.id === pageId);
|
||||||
@@ -5958,10 +6174,7 @@ Organization : OptiHK Limited
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const componentDisplayName = generateComponentDisplayName(parsedData.category || parsedData.name, {
|
const componentDisplayName = generateComponentDisplayName(parsedData.category || parsedData.name);
|
||||||
singularize: Boolean(parsedData.category),
|
|
||||||
abbreviate: Boolean(parsedData.category)
|
|
||||||
});
|
|
||||||
const newNode = {
|
const newNode = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
type: 'rotatableNode',
|
type: 'rotatableNode',
|
||||||
@@ -6402,8 +6615,11 @@ coordinate_system: gds_y_up
|
|||||||
canvas_size:
|
canvas_size:
|
||||||
width: ${Number(page.canvasSize?.width || DEFAULT_CANVAS_SIZE.width)}
|
width: ${Number(page.canvasSize?.width || DEFAULT_CANVAS_SIZE.width)}
|
||||||
height: ${Number(page.canvasSize?.height || DEFAULT_CANVAS_SIZE.height)}
|
height: ${Number(page.canvasSize?.height || DEFAULT_CANVAS_SIZE.height)}
|
||||||
project: ${currentProjectName}
|
origin:
|
||||||
name: ${page.name}
|
x: ${Number(canvasOrigin.x).toFixed(1)}
|
||||||
|
y: ${Number(canvasOrigin.y).toFixed(1)}
|
||||||
|
project: "${currentProjectName}"
|
||||||
|
name: "${page.name}"
|
||||||
type: ${page.type === 'project' ? 'project' : 'composite'}
|
type: ${page.type === 'project' ? 'project' : 'composite'}
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
|
||||||
@@ -6430,7 +6646,7 @@ ${instancesBlock}
|
|||||||
${elementsBlock}
|
${elementsBlock}
|
||||||
|
|
||||||
${bundlesBlock}`;
|
${bundlesBlock}`;
|
||||||
}, [currentProjectName, library, buildBundlesYaml]);
|
}, [currentProjectName, library, canvasOrigin.x, canvasOrigin.y, buildBundlesYaml]);
|
||||||
|
|
||||||
// Open or refresh a tab showing the generated SVG layout preview.
|
// Open or refresh a tab showing the generated SVG layout preview.
|
||||||
const openLayoutPreview = useCallback((cellName, svgUrl, layoutBounds) => {
|
const openLayoutPreview = useCallback((cellName, svgUrl, layoutBounds) => {
|
||||||
@@ -6590,14 +6806,6 @@ ${bundlesBlock}`;
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', width: '100%', height: '100%', userSelect: dragging ? 'none' : 'auto' }}>
|
<div style={{ display: 'flex', width: '100%', height: '100%', userSelect: dragging ? 'none' : 'auto' }}>
|
||||||
<div className="site-nav-actions">
|
|
||||||
<button className="mini-btn" onClick={() => { window.location.href = '/dashboard'; }}>
|
|
||||||
Dashboard
|
|
||||||
</button>
|
|
||||||
<button className="mini-btn" onClick={() => { window.location.href = '/logout'; }}>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
projectTreeItems={projectTreeItems}
|
projectTreeItems={projectTreeItems}
|
||||||
library={libraryWithCells} treeKey={treeKey} expanded={expanded}
|
library={libraryWithCells} treeKey={treeKey} expanded={expanded}
|
||||||
@@ -6634,6 +6842,14 @@ ${bundlesBlock}`;
|
|||||||
<button onClick={(e) => { e.stopPropagation(); closePage(page.id); }}>x</button>
|
<button onClick={(e) => { e.stopPropagation(); closePage(page.id); }}>x</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
|
||||||
|
<button className="mini-btn" onClick={() => { window.location.href = '/dashboard'; }}>
|
||||||
|
⊞ Dashboard
|
||||||
|
</button>
|
||||||
|
<button className="mini-btn" onClick={() => { window.location.href = '/logout'; }}>
|
||||||
|
↩ Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={canvasViewportRef}
|
ref={canvasViewportRef}
|
||||||
@@ -6774,7 +6990,7 @@ ${bundlesBlock}`;
|
|||||||
onNodeMouseDown={onNodeMouseDown}
|
onNodeMouseDown={onNodeMouseDown}
|
||||||
onEdgeMouseDown={handleReactFlowEdgeMouseDown}
|
onEdgeMouseDown={handleReactFlowEdgeMouseDown}
|
||||||
onNodeMouseUp={clearSpaceRotateNode}
|
onNodeMouseUp={clearSpaceRotateNode}
|
||||||
nodeTypes={{ rotatableNode: RotatableNode, portNode: PortNode, anchorNode: AnchorNode, canvasBoundaryNode: CanvasBoundaryNode, rulerPointNode: RulerPointNode, rulerMeasurementNode: RulerMeasurementNode }}
|
nodeTypes={{ rotatableNode: RotatableNode, portNode: PortNode, anchorNode: AnchorNode, canvasBoundaryNode: CanvasBoundaryNode, originMarkerNode: OriginMarkerNode, rulerPointNode: RulerPointNode, rulerMeasurementNode: RulerMeasurementNode }}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
nodeExtent={canvasNodeExtent}
|
nodeExtent={canvasNodeExtent}
|
||||||
snapToGrid={gridSnap}
|
snapToGrid={gridSnap}
|
||||||
@@ -6784,7 +7000,7 @@ ${bundlesBlock}`;
|
|||||||
elementsSelectable={true}
|
elementsSelectable={true}
|
||||||
connectionMode="loose"
|
connectionMode="loose"
|
||||||
connectionRadius={50}
|
connectionRadius={50}
|
||||||
minZoom={0.02}
|
minZoom={canvasMinZoom}
|
||||||
maxZoom={4}
|
maxZoom={4}
|
||||||
defaultViewport={{ x: 80, y: 80, zoom: 0.12 }}
|
defaultViewport={{ x: 80, y: 80, zoom: 0.12 }}
|
||||||
onMoveEnd={handleCanvasViewportMoveEnd}
|
onMoveEnd={handleCanvasViewportMoveEnd}
|
||||||
@@ -6817,6 +7033,7 @@ ${bundlesBlock}`;
|
|||||||
projectName={currentProjectName}
|
projectName={currentProjectName}
|
||||||
compositeNames={compositePageNames}
|
compositeNames={compositePageNames}
|
||||||
width={rightWidth}
|
width={rightWidth}
|
||||||
|
canvasOrigin={canvasOrigin}
|
||||||
onRenameComponent={renameComponent}
|
onRenameComponent={renameComponent}
|
||||||
onUpdateNode={handleUpdateNode}
|
onUpdateNode={handleUpdateNode}
|
||||||
onUpdateEdgeRoute={handleUpdateEdgeRoute}
|
onUpdateEdgeRoute={handleUpdateEdgeRoute}
|
||||||
|
|||||||
@@ -9,3 +9,26 @@
|
|||||||
4.Fixed the abnormal port shift after rotation.
|
4.Fixed the abnormal port shift after rotation.
|
||||||
|
|
||||||
5.Fixed the abnormal position of individual ports.
|
5.Fixed the abnormal position of individual ports.
|
||||||
|
|
||||||
|
06/15
|
||||||
|
|
||||||
|
Enhanced the contrast between the inside and outside of the canvas border.
|
||||||
|
|
||||||
|
Capped the maximum amount by which the canvas can be scaled down (set a minimum canvas size limit).
|
||||||
|
|
||||||
|
Labels/tags, once closed, will no longer be reloaded after a page refresh.
|
||||||
|
|
||||||
|
Modified the "select origin" functionality and set the default origin to the canvas position (500, 500).
|
||||||
|
|
||||||
|
Fixed a bug where the instance name counter was shared across different canvases within the same project.
|
||||||
|
|
||||||
|
06/16
|
||||||
|
|
||||||
|
Fixed a bug where component icons would not load when importing a YML file.
|
||||||
|
|
||||||
|
Added origin coordinates to the YML file; fixed a bug that caused component misalignment when opening a YML file with a different origin.
|
||||||
|
|
||||||
|
Fixed a bug where the default port would reload on every page refresh.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||